Skip to content

Commit 6cffc8b

Browse files
committed
env statusbar item, get env info via py script, refactoring
1 parent 43ee8e9 commit 6cffc8b

File tree

8 files changed

+494
-254
lines changed

8 files changed

+494
-254
lines changed

scripts/copyassets.js

+3
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ function copyAssests() {
4848
const htmlPath = path.join('browser', 'index.html');
4949
fs.copySync(path.join(srcDir, htmlPath), path.join(dest, htmlPath));
5050

51+
const envInfoPath = path.join('main', 'env_info.py');
52+
fs.copySync(path.join(srcDir, envInfoPath), path.join(dest, envInfoPath));
53+
5154
// Copy install scripts
5255
if (platform === 'darwin') {
5356
fs.copySync(path.join(path.resolve('./'), 'electron-builder-scripts', 'postinstall'), path.join(buildDir, 'pkg-scripts', 'postinstall'));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Copyright (c) Jupyter Development Team.
2+
// Distributed under the terms of the Modified BSD License.
3+
4+
import { VDomModel, VDomRenderer } from '@jupyterlab/apputils';
5+
import React from 'react';
6+
import { GroupItem, interactiveItem, TextItem } from '@jupyterlab/statusbar';
7+
import {
8+
pythonIcon
9+
} from '@jupyterlab/ui-components';
10+
11+
/**
12+
* A pure functional component for rendering environment status.
13+
*/
14+
function EnvironmentStatusComponent(
15+
props: EnvironmentStatusComponent.IProps
16+
): React.ReactElement<EnvironmentStatusComponent.IProps> {
17+
return (
18+
<GroupItem onClick={props.handleClick} spacing={2} title={props.description}>
19+
<pythonIcon.react title={''} top={'2px'} stylesheet={'statusBar'} />
20+
<TextItem source={props.name} />
21+
</GroupItem>
22+
);
23+
}
24+
25+
/**
26+
* A namespace for EnvironmentStatusComponent statics.
27+
*/
28+
namespace EnvironmentStatusComponent {
29+
/**
30+
* Props for the environment status component.
31+
*/
32+
export interface IProps {
33+
/**
34+
* A click handler for the environment status component. By default
35+
* we have it bring up the environment change dialog.
36+
*/
37+
handleClick: () => void;
38+
39+
/**
40+
* The name the environment.
41+
*/
42+
name: string;
43+
44+
/**
45+
* The description of the environment.
46+
*/
47+
description: string;
48+
}
49+
}
50+
51+
/**
52+
* A VDomRenderer widget for displaying the environment.
53+
*/
54+
export class EnvironmentStatus extends VDomRenderer<EnvironmentStatus.Model> {
55+
/**
56+
* Construct the environment status widget.
57+
*/
58+
constructor(opts: EnvironmentStatus.IOptions) {
59+
super(new EnvironmentStatus.Model());
60+
this.model.name = opts.name;
61+
this.model.description = opts.description;
62+
this._handleClick = opts.onClick;
63+
this.addClass(interactiveItem);
64+
}
65+
66+
/**
67+
* Render the environment status item.
68+
*/
69+
render() {
70+
if (this.model === null) {
71+
return null;
72+
} else {
73+
return (
74+
<EnvironmentStatusComponent
75+
name={this.model.name}
76+
description={this.model.description}
77+
handleClick={this._handleClick}
78+
/>
79+
);
80+
}
81+
}
82+
83+
private _handleClick: () => void;
84+
}
85+
86+
/**
87+
* A namespace for EnvironmentStatus statics.
88+
*/
89+
export namespace EnvironmentStatus {
90+
export class Model extends VDomModel {
91+
constructor() {
92+
super();
93+
94+
this._name = 'env';
95+
this._description = '';
96+
}
97+
98+
get name() {
99+
return this._name;
100+
}
101+
102+
set name(val: string) {
103+
const oldVal = this._name;
104+
if (oldVal === val) {
105+
return;
106+
}
107+
this._name = val;
108+
this.stateChanged.emit(void 0);
109+
}
110+
111+
get description(): string {
112+
return this._description;
113+
}
114+
set description(val: string) {
115+
const oldVal = this._description;
116+
if (oldVal === val) {
117+
return;
118+
}
119+
this._description = val;
120+
this.stateChanged.emit(void 0);
121+
}
122+
123+
private _name: string;
124+
private _description: string;
125+
}
126+
127+
/**
128+
* Options for creating a EnvironmentStatus object.
129+
*/
130+
export interface IOptions {
131+
/**
132+
* Environment name
133+
*/
134+
name: string;
135+
/**
136+
* Environment description
137+
*/
138+
description: string;
139+
/**
140+
* A click handler for the item. By default
141+
* we launch an environment selection dialog.
142+
*/
143+
onClick: () => void;
144+
}
145+
}

src/browser/extensions/desktop-extension/index.ts

+17-45
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,6 @@ import {
1818
JupyterFrontEndPlugin
1919
} from '@jupyterlab/application';
2020

21-
import {
22-
Widget
23-
} from '@lumino/widgets';
24-
2521
import {
2622
toArray
2723
} from '@lumino/algorithm';
@@ -35,6 +31,8 @@ import {
3531
} from '../../../asyncremote';
3632

3733
import { IAppRemoteInterface } from '../../../main/app';
34+
import { IPythonEnvironment } from 'src/main/tokens';
35+
import { EnvironmentStatus } from './envStatus';
3836

3937
async function waitForOriginUpdate(): Promise<void> {
4038
return new Promise((resolve) => {
@@ -47,41 +45,6 @@ async function waitForOriginUpdate(): Promise<void> {
4745
});
4846
}
4947

50-
class StatusBarItem extends Widget {
51-
static createNode(): HTMLElement {
52-
let node = document.createElement('div');
53-
let content = document.createElement('div');
54-
let button = document.createElement('button');
55-
button.textContent = 'Python Environment';
56-
button.onclick = () => {
57-
asyncRemoteRenderer.runRemoteMethod(IAppRemoteInterface.showPythonPathSelector, void(0));
58-
};
59-
content.appendChild(button);
60-
node.appendChild(content);
61-
return node;
62-
}
63-
64-
constructor(name: string) {
65-
super({ node: StatusBarItem.createNode() });
66-
this.setFlag(Widget.Flag.DisallowLayout);
67-
this.addClass('content');
68-
this.addClass(name.toLowerCase());
69-
this.title.label = name;
70-
this.title.closable = true;
71-
this.title.caption = `Long description for: ${name}`;
72-
}
73-
74-
get button(): HTMLButtonElement {
75-
return this.node.getElementsByTagName('button')[0] as HTMLButtonElement;
76-
}
77-
78-
protected onActivateRequest(msg: any): void {
79-
if (this.isAttached) {
80-
this.button.focus();
81-
}
82-
}
83-
}
84-
8548
const desktopExtension: JupyterFrontEndPlugin<void> = {
8649
id: 'jupyterlab-desktop.extensions.desktop',
8750
requires: [ICommandPalette, IMainMenu, ILabShell, IStatusBar],
@@ -105,19 +68,28 @@ const desktopExtension: JupyterFrontEndPlugin<void> = {
10568
{ command: 'check-for-updates' }
10669
], 20);
10770

108-
const statusItem = new StatusBarItem('Python');
71+
const changeEnvironment = async () => {
72+
asyncRemoteRenderer.runRemoteMethod(IAppRemoteInterface.showPythonPathSelector, void(0));
73+
};
74+
75+
const statusItem = new EnvironmentStatus({ name: 'env', description: '', onClick: changeEnvironment });
10976

11077
statusBar.registerStatusItem('jupyterlab-desktop-environment', {
11178
item: statusItem,
11279
align: 'left'
11380
});
11481

115-
asyncRemoteRenderer.runRemoteMethod(IAppRemoteInterface.getCurrentPythonPath, void(0)).then((path) => {
116-
statusItem.button.textContent = path === '' ? 'Python' : path;
117-
});
82+
const updateStatusItem = (env: IPythonEnvironment) => {
83+
statusItem.model.name = env.name;
84+
let packages = [];
85+
for (const name in env.versions) {
86+
packages.push(`${name}: ${env.versions[name]}`);
87+
}
88+
statusItem.model.description = `${env.name}\n${env.path}\n${packages.join(', ')}`;
89+
};
11890

119-
asyncRemoteRenderer.onRemoteEvent(IAppRemoteInterface.pythonPathChangedEvent, (newPath) => {
120-
statusItem.button.textContent = newPath;
91+
asyncRemoteRenderer.runRemoteMethod(IAppRemoteInterface.getCurrentPythonEnvironment, void(0)).then((env) => {
92+
updateStatusItem(env);
12193
});
12294

12395
const recreateLaunchers = () => {

src/main/app.ts

+39-14
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import {
2020
import log from 'electron-log';
2121

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

44-
getPythonPath(): Promise<string>;
46+
getPythonEnvironment(): Promise<IPythonEnvironment>;
4547
}
4648

4749
/**
@@ -100,8 +102,8 @@ namespace IAppRemoteInterface {
100102
id: 'JupyterLabDesktop-open-dev-tools'
101103
};
102104
export
103-
let getCurrentPythonPath: AsyncRemote.IMethod<void, string> = {
104-
id: 'JupyterLabDesktop-get-python-path'
105+
let getCurrentPythonEnvironment: AsyncRemote.IMethod<void, IPythonEnvironment> = {
106+
id: 'JupyterLabDesktop-get-python-env'
105107
};
106108
export
107109
let showPythonPathSelector: AsyncRemote.IMethod<void, void> = {
@@ -116,11 +118,13 @@ namespace IAppRemoteInterface {
116118
export
117119
class JupyterApplication implements IApplication, IStatefulService {
118120
readonly id = 'JupyterLabDesktop';
121+
private _registry: IRegistry;
119122

120123
/**
121124
* Construct the Jupyter application
122125
*/
123-
constructor() {
126+
constructor(registry: IRegistry) {
127+
this._registry = registry;
124128
this._registerListeners();
125129

126130
// Get application state from state db file.
@@ -147,6 +151,16 @@ class JupyterApplication implements IApplication, IStatefulService {
147151
if (this._applicationState.pythonPath === undefined) {
148152
this._applicationState.pythonPath = '';
149153
}
154+
let pythonPath = this._applicationState.pythonPath;
155+
if (pythonPath === '') {
156+
pythonPath = this._registry.getBundledPythonPath();
157+
}
158+
if (this._registry.validatePythonEnvironmentAtPath(pythonPath)) {
159+
this._registry.setDefaultPythonPath(pythonPath);
160+
this._applicationState.pythonPath = pythonPath;
161+
} else {
162+
this._showPythonSelectorDialog();
163+
}
150164
}
151165

152166
if (this._applicationState.checkForUpdatesAutomatically) {
@@ -157,10 +171,10 @@ class JupyterApplication implements IApplication, IStatefulService {
157171
});
158172
}
159173

160-
getPythonPath(): Promise<string> {
161-
return new Promise<string>((resolve, _reject) => {
174+
getPythonEnvironment(): Promise<IPythonEnvironment> {
175+
return new Promise<IPythonEnvironment>((resolve, _reject) => {
162176
this._appState.then((state: JSONObject) => {
163-
resolve(this._applicationState.pythonPath);
177+
resolve(this._registry.getCurrentPythonEnvironment());
164178
});
165179
});
166180
}
@@ -303,6 +317,10 @@ class JupyterApplication implements IApplication, IStatefulService {
303317
});
304318
});
305319

320+
ipcMain.handle('validate-python-path', (event, path) => {
321+
return this._registry.validatePythonEnvironmentAtPath(path);
322+
});
323+
306324
ipcMain.on('set-python-path', (event, path) => {
307325
this._applicationState.pythonPath = path;
308326
app.relaunch();
@@ -322,9 +340,9 @@ class JupyterApplication implements IApplication, IStatefulService {
322340
return Promise.resolve();
323341
});
324342

325-
asyncRemoteMain.registerRemoteMethod(IAppRemoteInterface.getCurrentPythonPath,
326-
(): Promise<string> => {
327-
return this.getPythonPath();
343+
asyncRemoteMain.registerRemoteMethod(IAppRemoteInterface.getCurrentPythonEnvironment,
344+
(): Promise<IPythonEnvironment> => {
345+
return this.getPythonEnvironment();
328346
});
329347

330348
asyncRemoteMain.registerRemoteMethod(IAppRemoteInterface.showPythonPathSelector,
@@ -458,7 +476,14 @@ class JupyterApplication implements IApplication, IStatefulService {
458476
}
459477
460478
function handleSave(el) {
461-
ipcRenderer.send('set-python-path', bundledRadio.checked ? '' : pythonPathInput.value);
479+
const useBundledEnv = bundledRadio.checked;
480+
if (!useBundledEnv) {
481+
ipcRenderer.invoke('validate-python-path', pythonPathInput.value).then((valid) => {
482+
ipcRenderer.send('set-python-path', pythonPathInput.value);
483+
});
484+
} else {
485+
ipcRenderer.send('set-python-path', '');
486+
}
462487
}
463488
464489
function handleCancel(el) {
@@ -544,10 +569,10 @@ namespace JupyterApplication {
544569
}
545570

546571
let service: IService = {
547-
requirements: [],
572+
requirements: ['IRegistry'],
548573
provides: 'IApplication',
549-
activate: (): IApplication => {
550-
return new JupyterApplication();
574+
activate: (registry: IRegistry): IApplication => {
575+
return new JupyterApplication(registry);
551576
}
552577
};
553578
export default service;

src/main/env_info.py

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import os, sys, json
2+
3+
env_type = 'system'
4+
env_name = 'python'
5+
6+
if (getattr(sys, "base_prefix", None) or getattr(sys, "real_prefix", None) or sys.prefix) != sys.prefix:
7+
env_type = 'venv'
8+
9+
if env_type != 'venv' and os.path.exists(os.path.join(sys.prefix, "conda-meta")):
10+
env_type = 'conda'
11+
12+
if env_type != 'system':
13+
env_name = os.path.basename(sys.prefix)
14+
15+
print(json.dumps({"type" : env_type, "name": env_name}))

0 commit comments

Comments
 (0)