Skip to content

Commit

Permalink
env statusbar item, get env info via py script, refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
mbektas committed Nov 5, 2021
1 parent 43ee8e9 commit 6cffc8b
Show file tree
Hide file tree
Showing 8 changed files with 494 additions and 254 deletions.
3 changes: 3 additions & 0 deletions scripts/copyassets.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ function copyAssests() {
const htmlPath = path.join('browser', 'index.html');
fs.copySync(path.join(srcDir, htmlPath), path.join(dest, htmlPath));

const envInfoPath = path.join('main', 'env_info.py');
fs.copySync(path.join(srcDir, envInfoPath), path.join(dest, envInfoPath));

// Copy install scripts
if (platform === 'darwin') {
fs.copySync(path.join(path.resolve('./'), 'electron-builder-scripts', 'postinstall'), path.join(buildDir, 'pkg-scripts', 'postinstall'));
Expand Down
145 changes: 145 additions & 0 deletions src/browser/extensions/desktop-extension/envStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.

import { VDomModel, VDomRenderer } from '@jupyterlab/apputils';
import React from 'react';
import { GroupItem, interactiveItem, TextItem } from '@jupyterlab/statusbar';
import {
pythonIcon
} from '@jupyterlab/ui-components';

/**
* A pure functional component for rendering environment status.
*/
function EnvironmentStatusComponent(
props: EnvironmentStatusComponent.IProps
): React.ReactElement<EnvironmentStatusComponent.IProps> {
return (
<GroupItem onClick={props.handleClick} spacing={2} title={props.description}>
<pythonIcon.react title={''} top={'2px'} stylesheet={'statusBar'} />
<TextItem source={props.name} />
</GroupItem>
);
}

/**
* A namespace for EnvironmentStatusComponent statics.
*/
namespace EnvironmentStatusComponent {
/**
* Props for the environment status component.
*/
export interface IProps {
/**
* A click handler for the environment status component. By default
* we have it bring up the environment change dialog.
*/
handleClick: () => void;

/**
* The name the environment.
*/
name: string;

/**
* The description of the environment.
*/
description: string;
}
}

/**
* A VDomRenderer widget for displaying the environment.
*/
export class EnvironmentStatus extends VDomRenderer<EnvironmentStatus.Model> {
/**
* Construct the environment status widget.
*/
constructor(opts: EnvironmentStatus.IOptions) {
super(new EnvironmentStatus.Model());
this.model.name = opts.name;
this.model.description = opts.description;
this._handleClick = opts.onClick;
this.addClass(interactiveItem);
}

/**
* Render the environment status item.
*/
render() {
if (this.model === null) {
return null;
} else {
return (
<EnvironmentStatusComponent
name={this.model.name}
description={this.model.description}
handleClick={this._handleClick}
/>
);
}
}

private _handleClick: () => void;
}

/**
* A namespace for EnvironmentStatus statics.
*/
export namespace EnvironmentStatus {
export class Model extends VDomModel {
constructor() {
super();

this._name = 'env';
this._description = '';
}

get name() {
return this._name;
}

set name(val: string) {
const oldVal = this._name;
if (oldVal === val) {
return;
}
this._name = val;
this.stateChanged.emit(void 0);
}

get description(): string {
return this._description;
}
set description(val: string) {
const oldVal = this._description;
if (oldVal === val) {
return;
}
this._description = val;
this.stateChanged.emit(void 0);
}

private _name: string;
private _description: string;
}

/**
* Options for creating a EnvironmentStatus object.
*/
export interface IOptions {
/**
* Environment name
*/
name: string;
/**
* Environment description
*/
description: string;
/**
* A click handler for the item. By default
* we launch an environment selection dialog.
*/
onClick: () => void;
}
}
62 changes: 17 additions & 45 deletions src/browser/extensions/desktop-extension/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@ import {
JupyterFrontEndPlugin
} from '@jupyterlab/application';

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

import {
toArray
} from '@lumino/algorithm';
Expand All @@ -35,6 +31,8 @@ import {
} from '../../../asyncremote';

import { IAppRemoteInterface } from '../../../main/app';
import { IPythonEnvironment } from 'src/main/tokens';
import { EnvironmentStatus } from './envStatus';

async function waitForOriginUpdate(): Promise<void> {
return new Promise((resolve) => {
Expand All @@ -47,41 +45,6 @@ async function waitForOriginUpdate(): Promise<void> {
});
}

class StatusBarItem extends Widget {
static createNode(): HTMLElement {
let node = document.createElement('div');
let content = document.createElement('div');
let button = document.createElement('button');
button.textContent = 'Python Environment';
button.onclick = () => {
asyncRemoteRenderer.runRemoteMethod(IAppRemoteInterface.showPythonPathSelector, void(0));
};
content.appendChild(button);
node.appendChild(content);
return node;
}

constructor(name: string) {
super({ node: StatusBarItem.createNode() });
this.setFlag(Widget.Flag.DisallowLayout);
this.addClass('content');
this.addClass(name.toLowerCase());
this.title.label = name;
this.title.closable = true;
this.title.caption = `Long description for: ${name}`;
}

get button(): HTMLButtonElement {
return this.node.getElementsByTagName('button')[0] as HTMLButtonElement;
}

protected onActivateRequest(msg: any): void {
if (this.isAttached) {
this.button.focus();
}
}
}

const desktopExtension: JupyterFrontEndPlugin<void> = {
id: 'jupyterlab-desktop.extensions.desktop',
requires: [ICommandPalette, IMainMenu, ILabShell, IStatusBar],
Expand All @@ -105,19 +68,28 @@ const desktopExtension: JupyterFrontEndPlugin<void> = {
{ command: 'check-for-updates' }
], 20);

const statusItem = new StatusBarItem('Python');
const changeEnvironment = async () => {
asyncRemoteRenderer.runRemoteMethod(IAppRemoteInterface.showPythonPathSelector, void(0));
};

const statusItem = new EnvironmentStatus({ name: 'env', description: '', onClick: changeEnvironment });

statusBar.registerStatusItem('jupyterlab-desktop-environment', {
item: statusItem,
align: 'left'
});

asyncRemoteRenderer.runRemoteMethod(IAppRemoteInterface.getCurrentPythonPath, void(0)).then((path) => {
statusItem.button.textContent = path === '' ? 'Python' : path;
});
const updateStatusItem = (env: IPythonEnvironment) => {
statusItem.model.name = env.name;
let packages = [];
for (const name in env.versions) {
packages.push(`${name}: ${env.versions[name]}`);
}
statusItem.model.description = `${env.name}\n${env.path}\n${packages.join(', ')}`;
};

asyncRemoteRenderer.onRemoteEvent(IAppRemoteInterface.pythonPathChangedEvent, (newPath) => {
statusItem.button.textContent = newPath;
asyncRemoteRenderer.runRemoteMethod(IAppRemoteInterface.getCurrentPythonEnvironment, void(0)).then((env) => {
updateStatusItem(env);
});

const recreateLaunchers = () => {
Expand Down
53 changes: 39 additions & 14 deletions src/main/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
import log from 'electron-log';

import { AsyncRemote, asyncRemoteMain } from '../asyncremote';
import { IPythonEnvironment } from './tokens';
import { IRegistry } from './registry';
import fetch from 'node-fetch';
import * as yaml from 'js-yaml';
import * as semver from 'semver';
Expand All @@ -41,7 +43,7 @@ interface IApplication {
*/
saveState: (service: IStatefulService, data: JSONValue) => Promise<void>;

getPythonPath(): Promise<string>;
getPythonEnvironment(): Promise<IPythonEnvironment>;
}

/**
Expand Down Expand Up @@ -100,8 +102,8 @@ namespace IAppRemoteInterface {
id: 'JupyterLabDesktop-open-dev-tools'
};
export
let getCurrentPythonPath: AsyncRemote.IMethod<void, string> = {
id: 'JupyterLabDesktop-get-python-path'
let getCurrentPythonEnvironment: AsyncRemote.IMethod<void, IPythonEnvironment> = {
id: 'JupyterLabDesktop-get-python-env'
};
export
let showPythonPathSelector: AsyncRemote.IMethod<void, void> = {
Expand All @@ -116,11 +118,13 @@ namespace IAppRemoteInterface {
export
class JupyterApplication implements IApplication, IStatefulService {
readonly id = 'JupyterLabDesktop';
private _registry: IRegistry;

/**
* Construct the Jupyter application
*/
constructor() {
constructor(registry: IRegistry) {
this._registry = registry;
this._registerListeners();

// Get application state from state db file.
Expand All @@ -147,6 +151,16 @@ class JupyterApplication implements IApplication, IStatefulService {
if (this._applicationState.pythonPath === undefined) {
this._applicationState.pythonPath = '';
}
let pythonPath = this._applicationState.pythonPath;
if (pythonPath === '') {
pythonPath = this._registry.getBundledPythonPath();
}
if (this._registry.validatePythonEnvironmentAtPath(pythonPath)) {
this._registry.setDefaultPythonPath(pythonPath);
this._applicationState.pythonPath = pythonPath;
} else {
this._showPythonSelectorDialog();
}
}

if (this._applicationState.checkForUpdatesAutomatically) {
Expand All @@ -157,10 +171,10 @@ class JupyterApplication implements IApplication, IStatefulService {
});
}

getPythonPath(): Promise<string> {
return new Promise<string>((resolve, _reject) => {
getPythonEnvironment(): Promise<IPythonEnvironment> {
return new Promise<IPythonEnvironment>((resolve, _reject) => {
this._appState.then((state: JSONObject) => {
resolve(this._applicationState.pythonPath);
resolve(this._registry.getCurrentPythonEnvironment());
});
});
}
Expand Down Expand Up @@ -303,6 +317,10 @@ class JupyterApplication implements IApplication, IStatefulService {
});
});

ipcMain.handle('validate-python-path', (event, path) => {
return this._registry.validatePythonEnvironmentAtPath(path);
});

ipcMain.on('set-python-path', (event, path) => {
this._applicationState.pythonPath = path;
app.relaunch();
Expand All @@ -322,9 +340,9 @@ class JupyterApplication implements IApplication, IStatefulService {
return Promise.resolve();
});

asyncRemoteMain.registerRemoteMethod(IAppRemoteInterface.getCurrentPythonPath,
(): Promise<string> => {
return this.getPythonPath();
asyncRemoteMain.registerRemoteMethod(IAppRemoteInterface.getCurrentPythonEnvironment,
(): Promise<IPythonEnvironment> => {
return this.getPythonEnvironment();
});

asyncRemoteMain.registerRemoteMethod(IAppRemoteInterface.showPythonPathSelector,
Expand Down Expand Up @@ -458,7 +476,14 @@ class JupyterApplication implements IApplication, IStatefulService {
}
function handleSave(el) {
ipcRenderer.send('set-python-path', bundledRadio.checked ? '' : pythonPathInput.value);
const useBundledEnv = bundledRadio.checked;
if (!useBundledEnv) {
ipcRenderer.invoke('validate-python-path', pythonPathInput.value).then((valid) => {
ipcRenderer.send('set-python-path', pythonPathInput.value);
});
} else {
ipcRenderer.send('set-python-path', '');
}
}
function handleCancel(el) {
Expand Down Expand Up @@ -544,10 +569,10 @@ namespace JupyterApplication {
}

let service: IService = {
requirements: [],
requirements: ['IRegistry'],
provides: 'IApplication',
activate: (): IApplication => {
return new JupyterApplication();
activate: (registry: IRegistry): IApplication => {
return new JupyterApplication(registry);
}
};
export default service;
15 changes: 15 additions & 0 deletions src/main/env_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import os, sys, json

env_type = 'system'
env_name = 'python'

if (getattr(sys, "base_prefix", None) or getattr(sys, "real_prefix", None) or sys.prefix) != sys.prefix:
env_type = 'venv'

if env_type != 'venv' and os.path.exists(os.path.join(sys.prefix, "conda-meta")):
env_type = 'conda'

if env_type != 'system':
env_name = os.path.basename(sys.prefix)

print(json.dumps({"type" : env_type, "name": env_name}))

0 comments on commit 6cffc8b

Please sign in to comment.