From 2c4f8ea8d8a966cc4bf82933043b4a2bf0eb1cf7 Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Sat, 9 Aug 2025 04:15:50 +0530 Subject: [PATCH 1/6] Ensure language metadata exists and resolve 422 --- src/index.ts | 62 +++++++++++++++++++++++++++++++++++++++++- src/pages/notebook.tsx | 28 ++++++++++++++++++- 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index e7453d5f..42bc9fa6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,7 +26,12 @@ import { ViewOnlyNotebookPanel } from './view-only'; -import { KERNEL_DISPLAY_NAMES, switchKernel } from './kernels'; +import { + KERNEL_DISPLAY_NAMES, + KERNEL_URL_TO_NAME, + KERNEL_NAME_TO_URL, + switchKernel +} from './kernels'; /** * Generate a shareable URL for the currently active notebook. @@ -75,6 +80,55 @@ async function showShareDialog(sharingService: SharingService, notebookContent: } } +function ensureLanguageMetadata( + panel: NotebookPanel | ViewOnlyNotebookPanel, + content: INotebookContent +): boolean { + const prev = JSON.stringify({ + ks: content.metadata?.kernelspec, + li: content.metadata?.language_info + }); + + // we go from URL alias (?kernel=python|r) to kernel name (xpython|xr) + const urlKernelParam = new URL(window.location.href).searchParams.get('kernel') || undefined; + const kernelFromUrl = urlKernelParam ? KERNEL_URL_TO_NAME[urlKernelParam] : undefined; + + const kernelFromSession = + panel instanceof NotebookPanel ? panel.sessionContext.session?.kernel?.name : undefined; + + const kernelName = + (content.metadata?.kernelspec as any)?.name || kernelFromSession || kernelFromUrl || 'xpython'; + + const language = + (content.metadata?.language_info as any)?.name || + (content.metadata?.kernelspec as any)?.language || + KERNEL_NAME_TO_URL[kernelName] || + 'python'; + + const display = KERNEL_DISPLAY_NAMES[kernelName] || (language === 'r' ? 'R' : 'Python'); + + content.metadata = { + ...content.metadata, + kernelspec: { + ...((content.metadata?.kernelspec as any) ?? {}), + name: kernelName, + display_name: display, + language + }, + language_info: { + ...((content.metadata?.language_info as any) ?? {}), + name: language + } + }; + + const next = JSON.stringify({ + ks: content.metadata.kernelspec, + li: content.metadata.language_info + }); + + return prev !== next; +} + /** * Notebook share/save handler. This function handles both sharing a new notebook and * updating an existing shared notebook. @@ -103,6 +157,12 @@ async function handleNotebookSharing( } return; } + // Ensure that the language metadata exists, even if the kernel has not started yet. + const patched = ensureLanguageMetadata(notebookPanel, notebookContent); + if (patched) { + notebookPanel.context.model.fromJSON(notebookContent); + } + if (sharedId) { console.log('Updating notebook:', sharedId); await sharingService.update(sharedId, notebookContent); diff --git a/src/pages/notebook.tsx b/src/pages/notebook.tsx index 4194d4da..f3b33c08 100644 --- a/src/pages/notebook.tsx +++ b/src/pages/notebook.tsx @@ -10,7 +10,7 @@ import { Commands } from '../commands'; import { SharingService } from '../sharing-service'; import { VIEW_ONLY_NOTEBOOK_FACTORY, IViewOnlyNotebookTracker } from '../view-only'; import { KernelSwitcherDropdownButton } from '../ui-components/KernelSwitcherDropdownButton'; -import { KERNEL_URL_TO_NAME } from '../kernels'; +import { KERNEL_URL_TO_NAME, KERNEL_NAME_TO_URL } from '../kernels'; /** * Maps the notebook content language to a kernel name. We currently @@ -157,6 +157,28 @@ export const notebookPlugin: JupyterFrontEndPlugin = { kernelName: desiredKernel }); + const panel = tracker.currentWidget; + if (panel) { + const json = panel.context.model.toJSON() as INotebookContent; + const language = KERNEL_NAME_TO_URL[desiredKernel] || 'python'; + + json.metadata = { + ...json.metadata, + kernelspec: { + ...((json.metadata?.kernelspec as any) ?? {}), + name: desiredKernel, + display_name: desiredKernel === 'xr' ? 'R' : 'Python', + language + }, + language_info: { + ...((json.metadata?.language_info as any) ?? {}), + name: language + } + }; + + panel.context.model.fromJSON(json); + } + console.log(`Created new notebook with kernel: ${desiredKernel}`); } catch (error) { console.error('Failed to create new notebook:', error); @@ -180,6 +202,10 @@ export const notebookPlugin: JupyterFrontEndPlugin = { name: kernelName, display_name: kernelName === 'xpython' ? 'Python 3' : 'R' }; + content.metadata.language_info = { + ...(content.metadata.language_info as any), + name: KERNEL_NAME_TO_URL[kernelName] || 'python' + }; const filename = `${(content.metadata?.name as string) || `Uploaded_${id}`}.ipynb`; From 4b62665b615157b82fcac7a8daf262df6d53cdaf Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Sat, 9 Aug 2025 06:01:56 +0530 Subject: [PATCH 2/6] Add file extension, language info, better typing and variable names --- src/index.ts | 71 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/src/index.ts b/src/index.ts index 42bc9fa6..758c6ac5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,7 +2,12 @@ import { ILabShell, JupyterFrontEnd, JupyterFrontEndPlugin } from '@jupyterlab/a import { INotebookTracker, NotebookPanel } from '@jupyterlab/notebook'; import { Dialog, showDialog, ReactWidget, Notification } from '@jupyterlab/apputils'; import { PageConfig } from '@jupyterlab/coreutils'; -import { INotebookContent } from '@jupyterlab/nbformat'; +import type { + INotebookContent, + INotebookMetadata, + IKernelspecMetadata, + ILanguageInfoMetadata +} from '@jupyterlab/nbformat'; import { customSidebar } from './sidebar'; import { SharingService } from './sharing-service'; @@ -84,49 +89,55 @@ function ensureLanguageMetadata( panel: NotebookPanel | ViewOnlyNotebookPanel, content: INotebookContent ): boolean { - const prev = JSON.stringify({ - ks: content.metadata?.kernelspec, - li: content.metadata?.language_info - }); + const meta = (content.metadata ?? {}) as INotebookMetadata; + const before = JSON.stringify({ ks: meta.kernelspec, li: meta.language_info }); - // we go from URL alias (?kernel=python|r) to kernel name (xpython|xr) const urlKernelParam = new URL(window.location.href).searchParams.get('kernel') || undefined; const kernelFromUrl = urlKernelParam ? KERNEL_URL_TO_NAME[urlKernelParam] : undefined; - const kernelFromSession = panel instanceof NotebookPanel ? panel.sessionContext.session?.kernel?.name : undefined; - const kernelName = - (content.metadata?.kernelspec as any)?.name || kernelFromSession || kernelFromUrl || 'xpython'; + const kernelName = meta.kernelspec?.name || kernelFromSession || kernelFromUrl || 'xpython'; - const language = - (content.metadata?.language_info as any)?.name || - (content.metadata?.kernelspec as any)?.language || - KERNEL_NAME_TO_URL[kernelName] || + const language: 'python' | 'r' = + (meta.language_info?.name as any) || + (meta.kernelspec as IKernelspecMetadata | undefined)?.language || + (KERNEL_NAME_TO_URL[kernelName] as any) || 'python'; const display = KERNEL_DISPLAY_NAMES[kernelName] || (language === 'r' ? 'R' : 'Python'); - content.metadata = { - ...content.metadata, - kernelspec: { - ...((content.metadata?.kernelspec as any) ?? {}), - name: kernelName, - display_name: display, - language - }, - language_info: { - ...((content.metadata?.language_info as any) ?? {}), - name: language - } - }; + const file_extension = + (meta.language_info as ILanguageInfoMetadata | undefined)?.file_extension ?? + (language === 'r' ? '.r' : '.py'); + const version = (meta.language_info as ILanguageInfoMetadata | undefined)?.version ?? '0'; + + const kernelspec: IKernelspecMetadata = { + ...(meta.kernelspec ?? {}), + name: kernelName, + display_name: display, + language + } as IKernelspecMetadata; + + const language_info: ILanguageInfoMetadata = { + ...(meta.language_info ?? {}), + name: language, + file_extension, + version + } as ILanguageInfoMetadata; - const next = JSON.stringify({ - ks: content.metadata.kernelspec, - li: content.metadata.language_info + content.metadata = { + ...(content.metadata ?? {}), + kernelspec, + language_info + } as INotebookMetadata; + + const after = JSON.stringify({ + ks: (content.metadata as INotebookMetadata).kernelspec, + li: (content.metadata as INotebookMetadata).language_info }); - return prev !== next; + return before !== after; } /** From 6a0d9530a9c88db650eb4ab63bb1b7ebc6670cdd Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Sat, 9 Aug 2025 06:37:00 +0530 Subject: [PATCH 3/6] More typing, add more comments, add docstring --- src/index.ts | 69 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 43 insertions(+), 26 deletions(-) diff --git a/src/index.ts b/src/index.ts index 758c6ac5..0583466f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -85,58 +85,75 @@ async function showShareDialog(sharingService: SharingService, notebookContent: } } +/** + * Ensure that the notebook metadata contains the necessary language and kernelspec information. + * This function will patch the metadata if it is missing or incomplete. This happens only for + * the initial load of the notebook, so that the metadata is always present even if the kernel + * has not started yet. Subsequent changes to the notebook will change the metadata we set here. + * @param panel - The notebook panel that we ensure the metadata for. + * @param content - The notebook content to patch, if necessary. + * @returns - a boolean indicating whether the metadata was patched. + */ function ensureLanguageMetadata( panel: NotebookPanel | ViewOnlyNotebookPanel, content: INotebookContent ): boolean { const meta = (content.metadata ?? {}) as INotebookMetadata; - const before = JSON.stringify({ ks: meta.kernelspec, li: meta.language_info }); + const before = JSON.stringify({ kernelspec: meta.kernelspec, languageInfo: meta.language_info }); - const urlKernelParam = new URL(window.location.href).searchParams.get('kernel') || undefined; + // We are trying to extract the kernel name from the URL, session, or metadata; because it is + // not guaranteed that the URL param might not be sanitised by the time we access it. + const urlKernelParam = new URL(window.location.href).searchParams.get('kernel') ?? undefined; const kernelFromUrl = urlKernelParam ? KERNEL_URL_TO_NAME[urlKernelParam] : undefined; const kernelFromSession = panel instanceof NotebookPanel ? panel.sessionContext.session?.kernel?.name : undefined; - const kernelName = meta.kernelspec?.name || kernelFromSession || kernelFromUrl || 'xpython'; - - const language: 'python' | 'r' = - (meta.language_info?.name as any) || - (meta.kernelspec as IKernelspecMetadata | undefined)?.language || - (KERNEL_NAME_TO_URL[kernelName] as any) || - 'python'; - - const display = KERNEL_DISPLAY_NAMES[kernelName] || (language === 'r' ? 'R' : 'Python'); - - const file_extension = - (meta.language_info as ILanguageInfoMetadata | undefined)?.file_extension ?? - (language === 'r' ? '.r' : '.py'); - const version = (meta.language_info as ILanguageInfoMetadata | undefined)?.version ?? '0'; - + const kernelName: 'xpython' | 'xr' = + (meta.kernelspec?.name as 'xpython' | 'xr' | undefined) ?? + (kernelFromSession as 'xpython' | 'xr' | undefined) ?? + (kernelFromUrl as 'xpython' | 'xr' | undefined) ?? + 'xpython'; + + const language: 'python' | 'r' = KERNEL_NAME_TO_URL[kernelName] as 'python' | 'r'; + const display: string = KERNEL_DISPLAY_NAMES[kernelName] ?? (language === 'r' ? 'R' : 'Python'); + + const file_extension: string = + meta.language_info?.file_extension && meta.language_info.file_extension.trim() !== '' + ? meta.language_info.file_extension + : language === 'r' + ? '.r' + : '.py'; + + // We use a placeholder until kernel.info arrives and we override it later + const version: string = + typeof meta.language_info?.version === 'string' && meta.language_info.version.trim() !== '' + ? meta.language_info.version + : '0'; + + // Once grabbed, we patch the metadata with the kernelspec and language_info + // If the metadata already exists, we merge it with the existing metadata. const kernelspec: IKernelspecMetadata = { ...(meta.kernelspec ?? {}), name: kernelName, display_name: display, language - } as IKernelspecMetadata; - + }; const language_info: ILanguageInfoMetadata = { ...(meta.language_info ?? {}), name: language, file_extension, version - } as ILanguageInfoMetadata; + }; - content.metadata = { + const patched: INotebookMetadata = { ...(content.metadata ?? {}), kernelspec, language_info - } as INotebookMetadata; + }; - const after = JSON.stringify({ - ks: (content.metadata as INotebookMetadata).kernelspec, - li: (content.metadata as INotebookMetadata).language_info - }); + content.metadata = patched; + const after = JSON.stringify({ ks: patched.kernelspec, li: patched.language_info }); return before !== after; } From 60a80ed846bfa4d9dee7205fcfc9cb2e5c902aca Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Sat, 9 Aug 2025 07:09:42 +0530 Subject: [PATCH 4/6] Drop notebook normalisation changes for now --- src/pages/notebook.tsx | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/src/pages/notebook.tsx b/src/pages/notebook.tsx index f3b33c08..4194d4da 100644 --- a/src/pages/notebook.tsx +++ b/src/pages/notebook.tsx @@ -10,7 +10,7 @@ import { Commands } from '../commands'; import { SharingService } from '../sharing-service'; import { VIEW_ONLY_NOTEBOOK_FACTORY, IViewOnlyNotebookTracker } from '../view-only'; import { KernelSwitcherDropdownButton } from '../ui-components/KernelSwitcherDropdownButton'; -import { KERNEL_URL_TO_NAME, KERNEL_NAME_TO_URL } from '../kernels'; +import { KERNEL_URL_TO_NAME } from '../kernels'; /** * Maps the notebook content language to a kernel name. We currently @@ -157,28 +157,6 @@ export const notebookPlugin: JupyterFrontEndPlugin = { kernelName: desiredKernel }); - const panel = tracker.currentWidget; - if (panel) { - const json = panel.context.model.toJSON() as INotebookContent; - const language = KERNEL_NAME_TO_URL[desiredKernel] || 'python'; - - json.metadata = { - ...json.metadata, - kernelspec: { - ...((json.metadata?.kernelspec as any) ?? {}), - name: desiredKernel, - display_name: desiredKernel === 'xr' ? 'R' : 'Python', - language - }, - language_info: { - ...((json.metadata?.language_info as any) ?? {}), - name: language - } - }; - - panel.context.model.fromJSON(json); - } - console.log(`Created new notebook with kernel: ${desiredKernel}`); } catch (error) { console.error('Failed to create new notebook:', error); @@ -202,10 +180,6 @@ export const notebookPlugin: JupyterFrontEndPlugin = { name: kernelName, display_name: kernelName === 'xpython' ? 'Python 3' : 'R' }; - content.metadata.language_info = { - ...(content.metadata.language_info as any), - name: KERNEL_NAME_TO_URL[kernelName] || 'python' - }; const filename = `${(content.metadata?.name as string) || `Uploaded_${id}`}.ipynb`; From a8f02f76adbabcc5bf86a3f002c664e0bb9a7502 Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Wed, 13 Aug 2025 23:33:02 +0530 Subject: [PATCH 5/6] Bump xeus-r with language file extension fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: MichaƂ Krassowski <5832902+krassowski@users.noreply.github.com> --- lite/xeus-environment.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lite/xeus-environment.yml b/lite/xeus-environment.yml index 2735a86f..0414bab4 100644 --- a/lite/xeus-environment.yml +++ b/lite/xeus-environment.yml @@ -4,7 +4,9 @@ channels: - https://repo.prefix.dev/conda-forge dependencies: - xeus-python - - xeus-r + # https://github.com/jupyter-xeus/xeus-r/commit/094d4e9d87b367f93c92792eee2950c89a171c0c + # Add https://github.com/jupyter-xeus/xeus-r/pull/161 until it is released + - git+https://github.com/jupyter-xeus/xeus-r.git@094d4e9d87b367f93c92792eee2950c89a171c0c - numpy - pandas - scipy From 9932ab5dd1d5074645ff9fdb0b41cce633ae992a Mon Sep 17 00:00:00 2001 From: Agriya Khetarpal <74401230+agriyakhetarpal@users.noreply.github.com> Date: Wed, 13 Aug 2025 23:53:37 +0530 Subject: [PATCH 6/6] Revert "Bump xeus-r with language file extension fix" This reverts commit a8f02f76adbabcc5bf86a3f002c664e0bb9a7502. --- lite/xeus-environment.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lite/xeus-environment.yml b/lite/xeus-environment.yml index 0414bab4..2735a86f 100644 --- a/lite/xeus-environment.yml +++ b/lite/xeus-environment.yml @@ -4,9 +4,7 @@ channels: - https://repo.prefix.dev/conda-forge dependencies: - xeus-python - # https://github.com/jupyter-xeus/xeus-r/commit/094d4e9d87b367f93c92792eee2950c89a171c0c - # Add https://github.com/jupyter-xeus/xeus-r/pull/161 until it is released - - git+https://github.com/jupyter-xeus/xeus-r.git@094d4e9d87b367f93c92792eee2950c89a171c0c + - xeus-r - numpy - pandas - scipy