Skip to content

Commit 2910974

Browse files
authored
add defensive handling of vscode api proposal usage (#174)
* add defensive handling of vscode api proposal usage * add tests * fix test
1 parent b47df83 commit 2910974

File tree

2 files changed

+226
-5
lines changed

2 files changed

+226
-5
lines changed

src/notebooks/controllers/vscodeNotebookController.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -173,10 +173,19 @@ export class VSCodeNotebookController implements Disposable, IVSCodeNotebookCont
173173
displayDataProvider
174174
);
175175

176-
try {
177-
controller.controller.variableProvider = jupyterVairablesProvider;
178-
} catch (ex) {
179-
logger.warn('Failed to attach variable provider', ex);
176+
// Only attach variable provider if the API is available
177+
// The notebookVariableProvider is a proposed API that:
178+
// - Works in extension development mode (F5 debugging)
179+
// - Does NOT work in published extensions from the Marketplace
180+
// - Requires users to manually enable it with --enable-proposed-api flag
181+
// See: https://code.visualstudio.com/api/advanced-topics/using-proposed-api
182+
// This check allows the extension to gracefully degrade when the API is unavailable
183+
if ('variableProvider' in controller.controller) {
184+
try {
185+
controller.controller.variableProvider = jupyterVairablesProvider;
186+
} catch (ex) {
187+
logger.warn('Failed to attach variable provider', ex);
188+
}
180189
}
181190

182191
return controller;

src/notebooks/controllers/vscodeNotebookController.unit.test.ts

Lines changed: 213 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,12 @@ import { KernelConnector } from './kernelConnector';
3737
import { ITrustedKernelPaths } from '../../kernels/raw/finder/types';
3838
import { IInterpreterService } from '../../platform/interpreter/contracts';
3939
import { PythonEnvironment } from '../../platform/pythonEnvironments/info';
40-
import { IConnectionDisplayDataProvider } from './types';
40+
import { IConnectionDisplayData, IConnectionDisplayDataProvider } from './types';
4141
import { ConnectionDisplayDataProvider } from './connectionDisplayData.node';
4242
import { mockedVSCodeNamespaces, resetVSCodeMocks } from '../../test/vscode-mock';
4343
import { Environment, PythonExtension } from '@vscode/python-extension';
4444
import { crateMockedPythonApi, whenResolveEnvironment } from '../../kernels/helpers.unit.test';
45+
import { IJupyterVariablesProvider } from '../../kernels/variables/types';
4546

4647
suite(`Notebook Controller`, function () {
4748
let controller: NotebookController;
@@ -544,4 +545,215 @@ suite(`Notebook Controller`, function () {
544545
});
545546
});
546547
});
548+
549+
suite('VSCodeNotebookController.create', function () {
550+
let kernelConnection: KernelConnectionMetadata;
551+
let kernelProvider: IKernelProvider;
552+
let context: IExtensionContext;
553+
let languageService: NotebookCellLanguageService;
554+
let configService: IConfigurationService;
555+
let extensionChecker: IPythonExtensionChecker;
556+
let serviceContainer: IServiceContainer;
557+
let displayDataProvider: IConnectionDisplayDataProvider;
558+
let jupyterVariablesProvider: IJupyterVariablesProvider;
559+
let disposables: IDisposable[] = [];
560+
let controller: NotebookController;
561+
let onDidChangeSelectedNotebooks: EventEmitter<{
562+
readonly notebook: NotebookDocument;
563+
readonly selected: boolean;
564+
}>;
565+
566+
setup(function () {
567+
resetVSCodeMocks();
568+
disposables.push(new Disposable(() => resetVSCodeMocks()));
569+
kernelConnection = mock<KernelConnectionMetadata>();
570+
kernelProvider = mock<IKernelProvider>();
571+
context = mock<IExtensionContext>();
572+
languageService = mock<NotebookCellLanguageService>();
573+
configService = mock<IConfigurationService>();
574+
extensionChecker = mock<IPythonExtensionChecker>();
575+
serviceContainer = mock<IServiceContainer>();
576+
displayDataProvider = mock<IConnectionDisplayDataProvider>();
577+
jupyterVariablesProvider = mock<IJupyterVariablesProvider>();
578+
controller = mock<NotebookController>();
579+
onDidChangeSelectedNotebooks = new EventEmitter<{
580+
readonly notebook: NotebookDocument;
581+
readonly selected: boolean;
582+
}>();
583+
disposables.push(onDidChangeSelectedNotebooks);
584+
585+
when(context.extensionUri).thenReturn(Uri.file('extension'));
586+
when(controller.onDidChangeSelectedNotebooks).thenReturn(onDidChangeSelectedNotebooks.event);
587+
when(displayDataProvider.getDisplayData(anything())).thenReturn({
588+
label: 'Test Kernel',
589+
description: 'Test Description',
590+
detail: 'Test Detail',
591+
category: 'Test Category',
592+
serverDisplayName: 'Test Server',
593+
onDidChange: new EventEmitter<IConnectionDisplayData>().event,
594+
dispose: () => {
595+
/* noop */
596+
}
597+
});
598+
when(
599+
mockedVSCodeNamespaces.notebooks.createNotebookController(
600+
anything(),
601+
anything(),
602+
anything(),
603+
anything(),
604+
anything()
605+
)
606+
).thenReturn(instance(controller));
607+
});
608+
609+
teardown(() => (disposables = dispose(disposables)));
610+
611+
test('Should attach variable provider when API is available', function () {
612+
// Arrange: Mock controller with variableProvider property
613+
const controllerWithApi = mock<NotebookController>();
614+
when(controllerWithApi.onDidChangeSelectedNotebooks).thenReturn(onDidChangeSelectedNotebooks.event);
615+
(instance(controllerWithApi) as any).variableProvider = undefined;
616+
617+
when(
618+
mockedVSCodeNamespaces.notebooks.createNotebookController(
619+
anything(),
620+
anything(),
621+
anything(),
622+
anything(),
623+
anything()
624+
)
625+
).thenReturn(instance(controllerWithApi));
626+
627+
// Act
628+
const result = VSCodeNotebookController.create(
629+
instance(kernelConnection),
630+
'test-id',
631+
'jupyter-notebook',
632+
instance(kernelProvider),
633+
instance(context),
634+
disposables,
635+
instance(languageService),
636+
instance(configService),
637+
instance(extensionChecker),
638+
instance(serviceContainer),
639+
instance(displayDataProvider),
640+
instance(jupyterVariablesProvider)
641+
);
642+
643+
// Assert
644+
assert.isDefined(result);
645+
assert.strictEqual(
646+
(result.controller as any).variableProvider,
647+
instance(jupyterVariablesProvider),
648+
'Variable provider should be attached when API is available'
649+
);
650+
});
651+
652+
test('Should not attach variable provider when API is not available', function () {
653+
// Arrange: Create a plain object without variableProvider property
654+
const controllerWithoutApi = {
655+
onDidChangeSelectedNotebooks: onDidChangeSelectedNotebooks.event,
656+
id: 'test-id',
657+
notebookType: 'jupyter-notebook',
658+
supportedLanguages: [],
659+
supportsExecutionOrder: true,
660+
description: '',
661+
detail: '',
662+
label: 'Test Kernel',
663+
dispose: () => {
664+
/* noop */
665+
},
666+
createNotebookCellExecution: () => ({}) as any,
667+
createNotebookExecution: () => ({}) as any,
668+
executeHandler: () => {
669+
/* noop */
670+
},
671+
interruptHandler: undefined,
672+
updateNotebookAffinity: () => {
673+
/* noop */
674+
},
675+
rendererScripts: [],
676+
onDidReceiveMessage: new EventEmitter<any>().event,
677+
postMessage: () => Promise.resolve(true),
678+
asWebviewUri: (uri: Uri) => uri
679+
// Note: no variableProvider property to simulate API not being available
680+
} as NotebookController;
681+
682+
when(
683+
mockedVSCodeNamespaces.notebooks.createNotebookController(
684+
anything(),
685+
anything(),
686+
anything(),
687+
anything(),
688+
anything()
689+
)
690+
).thenReturn(controllerWithoutApi);
691+
692+
// Act
693+
const result = VSCodeNotebookController.create(
694+
instance(kernelConnection),
695+
'test-id',
696+
'jupyter-notebook',
697+
instance(kernelProvider),
698+
instance(context),
699+
disposables,
700+
instance(languageService),
701+
instance(configService),
702+
instance(extensionChecker),
703+
instance(serviceContainer),
704+
instance(displayDataProvider),
705+
instance(jupyterVariablesProvider)
706+
);
707+
708+
// Assert
709+
assert.isDefined(result);
710+
assert.isFalse(
711+
'variableProvider' in result.controller,
712+
'Variable provider property should not exist when API is not available'
713+
);
714+
});
715+
716+
test('Should handle errors when attaching variable provider', function () {
717+
// Arrange: Mock controller that throws when setting variableProvider
718+
const controllerWithError = mock<NotebookController>();
719+
when(controllerWithError.onDidChangeSelectedNotebooks).thenReturn(onDidChangeSelectedNotebooks.event);
720+
721+
const controllerInstance = instance(controllerWithError);
722+
Object.defineProperty(controllerInstance, 'variableProvider', {
723+
set: () => {
724+
throw new Error('API not supported');
725+
},
726+
configurable: true
727+
});
728+
729+
when(
730+
mockedVSCodeNamespaces.notebooks.createNotebookController(
731+
anything(),
732+
anything(),
733+
anything(),
734+
anything(),
735+
anything()
736+
)
737+
).thenReturn(controllerInstance);
738+
739+
// Act - should not throw
740+
const result = VSCodeNotebookController.create(
741+
instance(kernelConnection),
742+
'test-id',
743+
'jupyter-notebook',
744+
instance(kernelProvider),
745+
instance(context),
746+
disposables,
747+
instance(languageService),
748+
instance(configService),
749+
instance(extensionChecker),
750+
instance(serviceContainer),
751+
instance(displayDataProvider),
752+
instance(jupyterVariablesProvider)
753+
);
754+
755+
// Assert
756+
assert.isDefined(result);
757+
});
758+
});
547759
});

0 commit comments

Comments
 (0)