你如何在 JupyterLab 中获取当前活动的笔记本名称?

How do you get the currently active notebook name in JupyterLab?

我正致力于在 JupyterLab 中创建一个服务器端扩展,并且一直在寻找一种方法来在我的 index.ts 文件中获取当前活动的笔记本名称。我发现 this existing extension that gets the currently active tab name with ILabShell and sets the browser tab to have the same name. In my case I'll be triggering the process with a button on the notebook toolbar so the active tab will always be a notebook. However, when I try to use the code in the activate section of my extension, I get TypeError: labShell.currentChanged is undefined where labShell is an instance of ILabShell. That extension doesn't support JupyterLab 3.0+ so I believe that's part of the problem. However, currentChanged for ILabShell is clearly defined here. What if anything can I change to make it work? Is there another way to accomplish what I'm trying to do? I'm aware of things like this 可以在笔记本中获取笔记本名称,但这并不是我想要做的。

Windows10,
节点 v14.17.0,
npm 6.14.13,
jlpm 1.21.1,
木星实验室 3.0.14

我使用这个示例服务器扩展作为模板:https://github.com/jupyterlab/extension-examples/tree/master/server-extension

index.ts 文件从现有扩展获取当前标签名称:

import {
  ILabShell,
  JupyterFrontEnd,
  JupyterFrontEndPlugin
} from '@jupyterlab/application';

import { Title, Widget } from '@lumino/widgets';

/**
 * Initialization data for the jupyterlab-active-as-tab-name extension.
 */
const extension: JupyterFrontEndPlugin<void> = {
  id: 'jupyterlab-active-as-tab-name',
  autoStart: true,
  requires: [ILabShell],
  activate: (app: JupyterFrontEnd, labShell: ILabShell) => {
    const onTitleChanged = (title: Title<Widget>) => {
      console.log('the JupyterLab main application:', title);
      document.title = title.label;
    };

    // Keep the session object on the status item up-to-date.
    labShell.currentChanged.connect((_, change) => {
      const { oldValue, newValue } = change;

      // Clean up after the old value if it exists,
      // listen for changes to the title of the activity
      if (oldValue) {
        oldValue.title.changed.disconnect(onTitleChanged);
      }
      if (newValue) {
        newValue.title.changed.connect(onTitleChanged);
      }
    });
  }
};

export default extension;

我的 index.ts 文件:

import {
  ILabShell,
  JupyterFrontEnd,
  JupyterFrontEndPlugin
} from '@jupyterlab/application';

import { ICommandPalette } from '@jupyterlab/apputils';

import { ILauncher } from '@jupyterlab/launcher';

import { requestAPI } from './handler';

import { ToolbarButton } from '@jupyterlab/apputils';

import { DocumentRegistry } from '@jupyterlab/docregistry';

import { INotebookModel, NotebookPanel } from '@jupyterlab/notebook';

import { IDisposable } from '@lumino/disposable';

import { Title, Widget } from '@lumino/widgets';

export class ButtonExtension implements DocumentRegistry.IWidgetExtension<NotebookPanel, INotebookModel> {
    
  constructor(app: JupyterFrontEnd) { 
      this.app = app;
  }
  
  readonly app: JupyterFrontEnd
    
  createNew(panel: NotebookPanel, context: DocumentRegistry.IContext<INotebookModel>): IDisposable {

    // dummy json data to test post requests
    const data2 = {"test message" : "message"}
    const options = {
      method: 'POST',
      body: JSON.stringify(data2),
      headers: {'Content-Type': 'application/json'
      }
    };
 
    // Create the toolbar button
    let mybutton = new ToolbarButton({ 
      label: 'Measure Energy Usage',
      onClick: async () => {

        // POST request to Jupyter server
        const dataToSend = { file: 'nbtest.ipynb' };
        try {
          const reply = await requestAPI<any>('hello', {
            body: JSON.stringify(dataToSend),
            method: 'POST'
          });
          console.log(reply);
        } catch (reason) {
          console.error(
            `Error on POST /jlab-ext-example/hello ${dataToSend}.\n${reason}`
          );
        }
        
        // sample POST request to svr.js
        fetch('http://localhost:9898/api', options);
      }
    });
    // Add the toolbar button to the notebook toolbar
    panel.toolbar.insertItem(10, 'MeasureEnergyUsage', mybutton);
    console.log("MeasEnerUsage activated");

    // The ToolbarButton class implements `IDisposable`, so the
    // button *is* the extension for the purposes of this method.
    return mybutton;
  }
}

/**
 * Initialization data for the server-extension-example extension.
 */
const extension: JupyterFrontEndPlugin<void> = {
  id: 'server-extension-example',
  autoStart: true,
  optional: [ILauncher],
  requires: [ICommandPalette, ILabShell],
  activate: async (
    app: JupyterFrontEnd,
    palette: ICommandPalette,
    launcher: ILauncher | null,
    labShell: ILabShell
  ) => {
    console.log('JupyterLab extension server-extension-example is activated!');
    const your_button = new ButtonExtension(app);
    app.docRegistry.addWidgetExtension('Notebook', your_button);
    
    // sample GET request to jupyter server
    try {
      const data = await requestAPI<any>('hello');
      console.log(data);
    } catch (reason) {
      console.error(`Error on GET /jlab-ext-example/hello.\n${reason}`);
    }

    // get name of active tab

    const onTitleChanged = (title: Title<Widget>) => {
      console.log('the JupyterLab main application:', title);
      document.title = title.label;
    };
    
    // Keep the session object on the status item up-to-date.
    labShell.currentChanged.connect((_, change) => {
      const { oldValue, newValue } = change;

      // Clean up after the old value if it exists,
      // listen for changes to the title of the activity
      if (oldValue) {
        oldValue.title.changed.disconnect(onTitleChanged);
      }
      if (newValue) {
        newValue.title.changed.connect(onTitleChanged);
      }
    });
  }
};

export default extension;

有两种修复方法和一种改进方法。我建议使用 (2) 和 (3)。

  1. 您在 activate 中的参数顺序错误。参数类型与签名函数没有神奇的匹配;相反,参数按照 requires 中给定的顺序传递,然后是 optional。这意味着您将收到:

    ...[JupyterFrontEnd, ICommandPalette, ILabShell, ILauncher]
    

    但你期待的是:

    ...[JupyterFrontEnd, ICommandPalette, ILauncher, ILabShell]
    

    换句话说,可选项总是在最后。没有静态类型检查,所以这是一个常见的错误来源 - 只需确保下次再次检查顺序(或 debug/console.log 看看你得到了什么)。

  2. 实际上,不需要ILabShell作为标记。请改用 app.shell 中的 ILabShell。这样您的扩展也将与使用 JupyterLab 组件构建的其他前端兼容。

    shell = app.shell as ILabShell
    
  3. (可选改进)安装 RetroLab 作为仅开发要求并使用 import type(这样它不是运行时要求)以确保与 [=23 的兼容性=]:

    import type { IRetroShell } from '@retrolab/application';
    // ... and then in `activate()`:
    shell = app.shell as ILabShell | IRetroShell
    

    明确一点:不这样做不会使您的扩展不兼容;它的作用是确保您不会根据将来 ILabShell 的实验室特定行为使其不兼容。

总的来说应该是这样的:

import {
  ILabShell,
  JupyterFrontEnd,
  JupyterFrontEndPlugin
} from '@jupyterlab/application';
import type { IRetroShell } from '@retrolab/application';

// ...

const extension: JupyterFrontEndPlugin<void> = {
  id: 'server-extension-example',
  autoStart: true,
  optional: [ILauncher],
  requires: [ICommandPalette],
  activate: async (
    app: JupyterFrontEnd,
    palette: ICommandPalette,
    launcher: ILauncher | null
  ) => {
    let shell = app.shell as ILabShell | IRetroShell ;
    shell.currentChanged.connect((_, change) => {
        console.log(change);
        // ...
    });
  }
};

export default extension;