Skip to content

Commit 0dfba61

Browse files
authored
Merge branch 'main' into tk/rename-jupyter-commands-deepnote
2 parents 1a1f1c2 + 31be353 commit 0dfba61

16 files changed

+654
-34
lines changed

icon.png

10.5 KB
Loading

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "vscode-deepnote",
33
"displayName": "Deepnote",
4-
"version": "0.3.0",
4+
"version": "1.0.0",
55
"description": "Deepnote notebook support.",
66
"publisher": "Deepnote",
77
"author": {

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
});

src/notebooks/deepnote/converters/inputConverters.ts

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,36 @@ import {
1818
import { DEEPNOTE_VSCODE_RAW_CONTENT_KEY } from './constants';
1919
import { formatInputBlockCellContent } from '../inputBlockContentFormatter';
2020

21+
/** Converts date strings to YYYY-MM-DD format, preserving values already in that format. */
22+
function normalizeDateString(dateValue: unknown): string {
23+
if (!dateValue || typeof dateValue !== 'string') {
24+
return '';
25+
}
26+
27+
if (/^\d{4}-\d{2}-\d{2}$/.test(dateValue)) {
28+
return dateValue;
29+
}
30+
31+
// Detect ISO-style strings that start with YYYY-MM-DD (e.g., "2025-09-30T00:00:00+02:00")
32+
// and extract just the date portion to avoid timezone shifts
33+
if (/^\d{4}-\d{2}-\d{2}/.test(dateValue)) {
34+
return dateValue.substring(0, 10);
35+
}
36+
37+
try {
38+
const date = new Date(dateValue);
39+
if (isNaN(date.getTime())) {
40+
return dateValue;
41+
}
42+
const year = date.getUTCFullYear();
43+
const month = String(date.getUTCMonth() + 1).padStart(2, '0');
44+
const day = String(date.getUTCDate()).padStart(2, '0');
45+
return `${year}-${month}-${day}`;
46+
} catch {
47+
return dateValue;
48+
}
49+
}
50+
2151
export abstract class BaseInputBlockConverter<T extends z.ZodObject> implements BlockConverter {
2252
abstract schema(): T;
2353
abstract getSupportedType(): string;
@@ -248,8 +278,20 @@ export class InputDateBlockConverter extends BaseInputBlockConverter<typeof Deep
248278
return cell;
249279
}
250280

251-
// Date blocks are readonly - edits are reverted by DeepnoteInputBlockEditProtection
252-
// Uses base class applyChangesToBlock which preserves existing metadata
281+
/**
282+
* Normalizes ISO date strings to YYYY-MM-DD format expected by createPythonCode.
283+
* Deepnote API may return dates like "2025-09-30T00:00:00.000Z".
284+
*/
285+
override applyChangesToBlock(block: DeepnoteBlock, _cell: NotebookCellData): void {
286+
const value = block.metadata?.deepnote_variable_value;
287+
288+
if (typeof value === 'string' && value) {
289+
const normalizedValue = normalizeDateString(value);
290+
this.updateBlockMetadata(block, { deepnote_variable_value: normalizedValue });
291+
} else {
292+
this.updateBlockMetadata(block, {});
293+
}
294+
}
253295
}
254296

255297
export class InputDateRangeBlockConverter extends BaseInputBlockConverter<typeof DeepnoteDateRangeInputMetadataSchema> {
@@ -271,8 +313,21 @@ export class InputDateRangeBlockConverter extends BaseInputBlockConverter<typeof
271313
return cell;
272314
}
273315

274-
// Date range blocks are readonly - edits are reverted by DeepnoteInputBlockEditProtection
275-
// Uses base class applyChangesToBlock which preserves existing metadata
316+
/**
317+
* Normalizes ISO date strings to YYYY-MM-DD format expected by createPythonCode.
318+
* Deepnote API may return dates like ["2025-09-30T00:00:00.000Z", "2025-10-16T00:00:00.000Z"].
319+
* Relative date strings like "past3months" are preserved as-is.
320+
*/
321+
override applyChangesToBlock(block: DeepnoteBlock, _cell: NotebookCellData): void {
322+
const value = block.metadata?.deepnote_variable_value;
323+
324+
if (Array.isArray(value) && value.length === 2) {
325+
const normalizedValue: [string, string] = [normalizeDateString(value[0]), normalizeDateString(value[1])];
326+
this.updateBlockMetadata(block, { deepnote_variable_value: normalizedValue });
327+
} else {
328+
this.updateBlockMetadata(block, {});
329+
}
330+
}
276331
}
277332

278333
export class InputFileBlockConverter extends BaseInputBlockConverter<typeof DeepnoteFileInputMetadataSchema> {

0 commit comments

Comments
 (0)