Trigger Export Executable Script Command from index.ts

Hello all, first time posting here. I’m working on creating a JupyterLab extension that will eventually let users test the power usage of their code, and the first step is to create a button that downloads the contents of a Notebook as an executable script.

This can already be done through File > Export Notebook As… > Executable Script or through the Command Palette with Export Notebook: Executable Script, so I suspect it should be fairly straightforward. I’m having trouble getting this command to run from my index.ts file however; any help would be very much appreciated. I believe my main problem is not knowing how to pass the right argument to notebook:export-to-format but I may be doing something else wrong as well.

I set up the environment with this command and cookiecutter from the Extension Tutorial (Extension Tutorial — JupyterLab 3.0.16 documentation):

conda create -n greencode-ext3 --override-channels --strict-channel-priority -c conda-forge -c anaconda jupyterlab=3 cookiecutter nodejs jupyter-packaging git

cookiecutter GitHub - jupyterlab/extension-cookiecutter-ts: A cookiecutter recipe for JupyterLab extensions in Typescript

Here’s my index.ts file:

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

import { ITranslator } from '@jupyterlab/translation';

import { ToolbarButton } from '@jupyterlab/apputils';
import { DocumentRegistry } from '@jupyterlab/docregistry';
import { INotebookModel, NotebookPanel, INotebookTracker } from '@jupyterlab/notebook';

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

export class ButtonExtension implements DocumentRegistry.IWidgetExtension<NotebookPanel, INotebookModel> {
    
  constructor(app: JupyterLab) {
      this.app = app;
  }
    
  readonly app: JupyterLab;

  createNew(panel: NotebookPanel, context: DocumentRegistry.IContext<INotebookModel>): IDisposable {
     const { commands } = this.app;
     const command = 'notebook:export-to-format'

     // Create the toolbar button
     let mybutton = new ToolbarButton({
         label: 'Measure Power Usage',
         onClick: () => { 
             alert('You did it'); // alert to confirm button works
             commands.execute(command, {formatLabel: 'script'});
          }
       });

    // Add the toolbar button to the notebook toolbar
    panel.toolbar.insertItem(10, 'MeasurePowerUsage', mybutton);
    console.log("MeasPowerUsage activated");

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

/**
 * Initialization data for the greencode-ext3 extension.
 */
const yourPlugin: JupyterFrontEndPlugin<void> = {
  id: '@your-org/plugin-name',
  autoStart: true,
  requires: [ITranslator, INotebookTracker],
  activate: (app: JupyterFrontEnd) => {
    const your_button = new ButtonExtension(new JupyterLab);
    app.docRegistry.addWidgetExtension('Notebook', your_button);
  }
}

export default yourPlugin;
1 Like

Welcome! Do you have any compilation errors, or is it working but not doing what you would expect?

The is fragment looks odd to me:

I think that you should pass app object with an instance rather than creation the a new JupyterLab instance (and besides it would be missing parentheses). But I guess it might be just a copy-paste error.

Thanks for the reply! Yes, it compiles ok, but when I press the button nothing happens except for the dummy alert. I did initially try new ButtonExtension(app) there but got “error TS2345: Argument of type ‘JupyterFrontEnd<IShell, “desktop” | “mobile”>’ is not assignable to parameter of type ‘JupyterLab’.
Type ‘JupyterFrontEnd<IShell, “desktop” | “mobile”>’ is missing the following properties from type ‘JupyterLab’: registerPluginErrors, status, info, paths, and 4 more.”

Then I tried ButtonExtension(JupyterLab) which gave me “error TS2345: Argument of type ‘typeof JupyterLab’ is not assignable to parameter of type ‘JupyterLab’.
Type ‘typeof JupyterLab’ is missing the following properties from type ‘JupyterLab’: namespace, registerPluginErrors, restored, status, and 38 more.” The same error message suggested to add “new” so that’s what I did and it fixed the compilation error.

Passing app is the correct way - it does not work because you are creating a custom instance of the application which does not connect to the existing instance. Just update your constructor to use the more general type this is JupyterFrontEnd (JupyterLab is a subclass of JupyterFrontEnd)

  constructor(app: JupyterFrontEnd) {
      this.app = app;
  }
    
  readonly app: JupyterFrontEnd;

Please let me know if it works well for you.

Edit: I made a copy-paste error when writing this a minute ago, it is now corrected (it should be as it is now).

Ah I see–yes, the code compiles fine with those changes. That may have been part of the problem. The button still doesn’t download the executable script the way I’d like it to though. I suspect it’s something to do with how I’m passing the argument to commands.execute, since I can replace formatLabel and 'script' with any gibberish I want and the code still compiles.

You are right - the command arguments to execute can be basically any reasonable JSON. Looking at the code of the notebook:export-to-format command, I think that you should use format rather than formatLabel to export the currently active notebook:

The getCurrent function called there also looks at activate argument (which I think should be set to true in your case but I may be wrong).

So something like:

commands.execute(command, {format: 'script', 'activate': true});

Aha, that works beautifully! Thanks so much.

1 Like

Follow-up question on this–is there a way to have the .py file go to the same folder in JupyterLab as the notebook that’s being converted rather than the user’s Downloads folder?

It is certainly possible by creating a server-side extension that would instruct nbconvert to output the file there. Have a look at cookiecutter linked from extension tutorial - it should have a stub for creating a server extension.

Also, just in case GitHub - mwouts/jupytext: Jupyter Notebooks as Markdown Documents, Julia, Python or R scripts might be of your interest.

I’ll look into these, thanks!

Is there a way to use nbconvert directly in TypeScript, or do you have to use either a Python script or a bash command?

Maybe instead of doing the server extension you could just send a request to the url and use JupyterLab APIs to open a new file and then to save the context from that (nbconvert output) url into it? Pseudocode below:

const url = PageConfig.getNBConvertURL({
  format: args['format'] as string,
  download: false,
  path: current.context.path
});
(
  fetch(url)
  .then(response => openNewFileAndWriteToItWithJupterLabAPI(current.context.path + '.py', response.text))
)

As for openNewFileAndWriteToItWithJupterLabAPI it could either use the “upload” method of FileBrowser, or just create a new blank file, open it with text editor, write to it and close it quickly. It might be possible to do so without opening the file in the UI.