Skip to content

Commit 6d205f4

Browse files
authored
Prompt to launch TensorBoard when active Python file or ipynb contains a tensorboard import (#14884)
1 parent 0bc0e38 commit 6d205f4

File tree

8 files changed

+223
-21
lines changed

8 files changed

+223
-21
lines changed

src/client/telemetry/importTracker.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -67,19 +67,6 @@ export class ImportTracker implements IExtensionSingleActivationService {
6767
this.documentManager.textDocuments.forEach((d) => this.onOpenedOrSavedDocument(d));
6868
}
6969

70-
private getDocumentLines(document: TextDocument): (string | undefined)[] {
71-
const array = Array<string>(Math.min(document.lineCount, MAX_DOCUMENT_LINES)).fill('');
72-
return array
73-
.map((_a: string, i: number) => {
74-
const line = document.lineAt(i);
75-
if (line && !line.isEmptyOrWhitespace) {
76-
return line.text;
77-
}
78-
return undefined;
79-
})
80-
.filter((f: string | undefined) => f);
81-
}
82-
8370
private onOpenedOrSavedDocument(document: TextDocument) {
8471
// Make sure this is a Python file.
8572
if (path.extname(document.fileName) === '.py') {
@@ -112,7 +99,7 @@ export class ImportTracker implements IExtensionSingleActivationService {
11299
@captureTelemetry(EventName.HASHED_PACKAGE_PERF)
113100
private checkDocument(document: TextDocument) {
114101
this.pendingChecks.delete(document.fileName);
115-
const lines = this.getDocumentLines(document);
102+
const lines = getDocumentLines(document);
116103
this.lookForImports(lines);
117104
}
118105

@@ -152,3 +139,16 @@ export class ImportTracker implements IExtensionSingleActivationService {
152139
}
153140
}
154141
}
142+
143+
export function getDocumentLines(document: TextDocument): (string | undefined)[] {
144+
const array = Array<string>(Math.min(document.lineCount, MAX_DOCUMENT_LINES)).fill('');
145+
return array
146+
.map((_a: string, i: number) => {
147+
const line = document.lineAt(i);
148+
if (line && !line.isEmptyOrWhitespace) {
149+
return line.text;
150+
}
151+
return undefined;
152+
})
153+
.filter((f: string | undefined) => f);
154+
}

src/client/tensorBoard/serviceRegistry.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
import { IExtensionSingleActivationService } from '../activation/types';
55
import { IServiceManager } from '../ioc/types';
66
import { TensorBoardFileWatcher } from './tensorBoardFileWatcher';
7+
import { TensorBoardImportTracker } from './tensorBoardImportTracker';
78
import { TensorBoardPrompt } from './tensorBoardPrompt';
89
import { TensorBoardSessionProvider } from './tensorBoardSessionProvider';
10+
import { ITensorBoardImportTracker } from './types';
911

1012
export function registerTypes(serviceManager: IServiceManager) {
1113
serviceManager.addSingleton<IExtensionSingleActivationService>(
@@ -17,4 +19,6 @@ export function registerTypes(serviceManager: IServiceManager) {
1719
TensorBoardFileWatcher
1820
);
1921
serviceManager.addSingleton<TensorBoardPrompt>(TensorBoardPrompt, TensorBoardPrompt);
22+
serviceManager.addSingleton<ITensorBoardImportTracker>(ITensorBoardImportTracker, TensorBoardImportTracker);
23+
serviceManager.addBinding(ITensorBoardImportTracker, IExtensionSingleActivationService);
2024
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { inject, injectable } from 'inversify';
2+
import { noop } from 'lodash';
3+
import * as path from 'path';
4+
import { Event, EventEmitter, TextEditor, window } from 'vscode';
5+
import { IExtensionSingleActivationService } from '../activation/types';
6+
import { IDocumentManager } from '../common/application/types';
7+
import { IDisposableRegistry } from '../common/types';
8+
import { getDocumentLines } from '../telemetry/importTracker';
9+
import { ITensorBoardImportTracker } from './types';
10+
11+
// While it is uncommon for users to `import tensorboard`, TensorBoard is frequently
12+
// included as a submodule of other packages, e.g. torch.utils.tensorboard.
13+
// This is a modified version of the regex from src/client/telemetry/importTracker.ts
14+
// in order to match on imported submodules as well, since the original regex only
15+
// matches the 'main' module.
16+
const ImportRegEx = /^\s*from (?<fromImport>\w+(?:\.\w+)*) import (?<fromImportTarget>\w+(?:, \w+)*)(?: as \w+)?|import (?<importImport>\w+(?:, \w+)*)(?: as \w+)?$/;
17+
18+
@injectable()
19+
export class TensorBoardImportTracker implements ITensorBoardImportTracker, IExtensionSingleActivationService {
20+
private pendingChecks = new Map<string, NodeJS.Timer | number>();
21+
private _onDidImportTensorBoard = new EventEmitter<void>();
22+
23+
constructor(
24+
@inject(IDocumentManager) private documentManager: IDocumentManager,
25+
@inject(IDisposableRegistry) private disposables: IDisposableRegistry
26+
) {
27+
this.documentManager.onDidChangeActiveTextEditor(
28+
(e) => this.onChangedActiveTextEditor(e),
29+
this,
30+
this.disposables
31+
);
32+
}
33+
34+
// Fires when the active text editor contains a tensorboard import.
35+
public get onDidImportTensorBoard(): Event<void> {
36+
return this._onDidImportTensorBoard.event;
37+
}
38+
39+
public dispose() {
40+
this.pendingChecks.clear();
41+
}
42+
43+
public async activate(): Promise<void> {
44+
// Process active text editor with a timeout delay
45+
this.onChangedActiveTextEditor(window.activeTextEditor);
46+
}
47+
48+
private onChangedActiveTextEditor(editor: TextEditor | undefined) {
49+
if (!editor || !editor.document) {
50+
return;
51+
}
52+
const document = editor.document;
53+
if (
54+
(path.extname(document.fileName) === '.ipynb' && document.languageId === 'python') ||
55+
path.extname(document.fileName) === '.py'
56+
) {
57+
const lines = getDocumentLines(document);
58+
this.lookForImports(lines);
59+
}
60+
}
61+
62+
private lookForImports(lines: (string | undefined)[]) {
63+
try {
64+
for (const s of lines) {
65+
const matches = s ? ImportRegEx.exec(s) : null;
66+
if (matches === null || matches.groups === undefined) {
67+
continue;
68+
}
69+
let componentsToCheck: string[] = [];
70+
if (matches.groups.fromImport && matches.groups.fromImportTarget) {
71+
// from x.y.z import u, v, w
72+
componentsToCheck = matches.groups.fromImport
73+
.split('.')
74+
.concat(matches.groups.fromImportTarget.split(','));
75+
} else if (matches.groups.importImport) {
76+
// import package1, package2, ...
77+
componentsToCheck = matches.groups.importImport.split(',');
78+
}
79+
for (const component of componentsToCheck) {
80+
if (component && component.trim() === 'tensorboard') {
81+
this._onDidImportTensorBoard.fire();
82+
return;
83+
}
84+
}
85+
}
86+
} catch {
87+
// Don't care about failures.
88+
noop();
89+
}
90+
}
91+
}

src/client/tensorBoard/tensorBoardPrompt.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
import { inject, injectable } from 'inversify';
55
import { IApplicationShell, ICommandManager } from '../common/application/types';
66
import { Commands } from '../common/constants';
7-
import { IPersistentState, IPersistentStateFactory } from '../common/types';
7+
import { IDisposableRegistry, IPersistentState, IPersistentStateFactory } from '../common/types';
88
import { Common, TensorBoard } from '../common/utils/localize';
9+
import { ITensorBoardImportTracker } from './types';
910

1011
enum TensorBoardPromptStateKeys {
1112
ShowNativeTensorBoardPrompt = 'showNativeTensorBoardPrompt'
@@ -15,22 +16,26 @@ enum TensorBoardPromptStateKeys {
1516
export class TensorBoardPrompt {
1617
private state: IPersistentState<boolean>;
1718
private enabled: Promise<boolean> | undefined;
19+
private enabledInCurrentSession: boolean = true;
1820
private waitingForUserSelection: boolean = false;
1921

2022
constructor(
2123
@inject(IApplicationShell) private applicationShell: IApplicationShell,
2224
@inject(ICommandManager) private commandManager: ICommandManager,
25+
@inject(ITensorBoardImportTracker) private importTracker: ITensorBoardImportTracker,
26+
@inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry,
2327
@inject(IPersistentStateFactory) private persistentStateFactory: IPersistentStateFactory
2428
) {
2529
this.state = this.persistentStateFactory.createWorkspacePersistentState<boolean>(
2630
TensorBoardPromptStateKeys.ShowNativeTensorBoardPrompt,
2731
true
2832
);
2933
this.enabled = this.isPromptEnabled();
34+
this.importTracker.onDidImportTensorBoard(this.showNativeTensorBoardPrompt, this, this.disposableRegistry);
3035
}
3136

3237
public async showNativeTensorBoardPrompt() {
33-
if ((await this.enabled) && !this.waitingForUserSelection) {
38+
if ((await this.enabled) && this.enabledInCurrentSession && !this.waitingForUserSelection) {
3439
const yes = Common.bannerLabelYes();
3540
const no = Common.bannerLabelNo();
3641
const doNotAskAgain = Common.doNotShowAgain();
@@ -41,10 +46,10 @@ export class TensorBoardPrompt {
4146
...options
4247
);
4348
this.waitingForUserSelection = false;
49+
this.enabledInCurrentSession = false;
4450
switch (selection) {
4551
case yes:
4652
await this.commandManager.executeCommand(Commands.LaunchTensorBoard);
47-
await this.disablePrompt();
4853
break;
4954
case doNotAskAgain:
5055
await this.disablePrompt();

src/client/tensorBoard/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { Event } from 'vscode';
5+
6+
export const ITensorBoardImportTracker = Symbol('ITensorBoardImportTracker');
7+
export interface ITensorBoardImportTracker {
8+
onDidImportTensorBoard: Event<void>;
9+
}

src/test/startPage/mockDocument.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,20 @@ export class MockDocument implements TextDocument {
5151
private _contents: string = '';
5252
private _isUntitled = false;
5353
private _isDirty = false;
54+
private _language = 'python';
5455
private _onSave: (doc: TextDocument) => Promise<boolean>;
5556

56-
constructor(contents: string, fileName: string, onSave: (doc: TextDocument) => Promise<boolean>) {
57+
constructor(
58+
contents: string,
59+
fileName: string,
60+
onSave: (doc: TextDocument) => Promise<boolean>,
61+
language?: string
62+
) {
5763
this._uri = Uri.file(fileName);
5864
this._contents = contents;
5965
this._lines = this.createLines();
6066
this._onSave = onSave;
67+
this._language = language ?? this._language;
6168
}
6269

6370
public setContent(contents: string) {
@@ -85,7 +92,7 @@ export class MockDocument implements TextDocument {
8592
return this._isUntitled;
8693
}
8794
public get languageId(): string {
88-
return 'python';
95+
return this._language;
8996
}
9097
public get version(): number {
9198
return this._version;

src/test/startPage/mockDocumentManager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,12 @@ export class MockDocumentManager implements IDocumentManager {
9494
throw new Error('Method not implemented.');
9595
}
9696

97-
public addDocument(code: string, file: string) {
97+
public addDocument(code: string, file: string, language?: string) {
9898
let existing = this.textDocuments.find((d) => d.uri.fsPath === file) as MockDocument;
9999
if (existing) {
100100
existing.setContent(code);
101101
} else {
102-
existing = new MockDocument(code, file, this.saveDocument);
102+
existing = new MockDocument(code, file, this.saveDocument, language);
103103
this.textDocuments.push(existing);
104104
}
105105
return existing;
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import * as sinon from 'sinon';
2+
import { TensorBoardImportTracker } from '../../client/tensorBoard/tensorBoardImportTracker';
3+
import { MockDocumentManager } from '../startPage/mockDocumentManager';
4+
5+
suite('TensorBoard import tracker', () => {
6+
let documentManager: MockDocumentManager;
7+
let tensorBoardImportTracker: TensorBoardImportTracker;
8+
let onDidImportTensorBoardListener: sinon.SinonExpectation;
9+
10+
setup(() => {
11+
documentManager = new MockDocumentManager();
12+
tensorBoardImportTracker = new TensorBoardImportTracker(documentManager, []);
13+
onDidImportTensorBoardListener = sinon.expectation.create('onDidImportTensorBoardListener');
14+
tensorBoardImportTracker.onDidImportTensorBoard(onDidImportTensorBoardListener);
15+
});
16+
17+
test('Simple tensorboard import in Python file', async () => {
18+
const document = documentManager.addDocument('import tensorboard', 'foo.py');
19+
await documentManager.showTextDocument(document);
20+
await tensorBoardImportTracker.activate();
21+
onDidImportTensorBoardListener.once().verify();
22+
});
23+
test('Simple tensorboard import in Python ipynb', async () => {
24+
const document = documentManager.addDocument('import tensorboard', 'foo.ipynb');
25+
await documentManager.showTextDocument(document);
26+
await tensorBoardImportTracker.activate();
27+
onDidImportTensorBoardListener.once().verify();
28+
});
29+
test('`from x.y.tensorboard import z` import', async () => {
30+
const document = documentManager.addDocument('from torch.utils.tensorboard import SummaryWriter', 'foo.py');
31+
await documentManager.showTextDocument(document);
32+
await tensorBoardImportTracker.activate();
33+
onDidImportTensorBoardListener.once().verify();
34+
});
35+
test('`from x.y import tensorboard` import', async () => {
36+
const document = documentManager.addDocument('from torch.utils import tensorboard', 'foo.py');
37+
await documentManager.showTextDocument(document);
38+
await tensorBoardImportTracker.activate();
39+
onDidImportTensorBoardListener.once().verify();
40+
});
41+
test('`import x, y` import', async () => {
42+
const document = documentManager.addDocument('import tensorboard, tensorflow', 'foo.py');
43+
await documentManager.showTextDocument(document);
44+
await tensorBoardImportTracker.activate();
45+
onDidImportTensorBoardListener.once().verify();
46+
});
47+
test('`import pkg as _` import', async () => {
48+
const document = documentManager.addDocument('import tensorboard as tb', 'foo.py');
49+
await documentManager.showTextDocument(document);
50+
await tensorBoardImportTracker.activate();
51+
onDidImportTensorBoardListener.once().verify();
52+
});
53+
test('Fire on changed text editor', async () => {
54+
await tensorBoardImportTracker.activate();
55+
const document = documentManager.addDocument('import tensorboard as tb', 'foo.py');
56+
await documentManager.showTextDocument(document);
57+
onDidImportTensorBoardListener.once().verify();
58+
});
59+
test('Do not fire event if no tensorboard import', async () => {
60+
const document = documentManager.addDocument('import tensorflow as tf\nfrom torch.utils import foo', 'foo.py');
61+
await documentManager.showTextDocument(document);
62+
await tensorBoardImportTracker.activate();
63+
onDidImportTensorBoardListener.never().verify();
64+
});
65+
test('Do not fire event if language is not Python', async () => {
66+
const document = documentManager.addDocument(
67+
'import tensorflow as tf\nfrom torch.utils import foo',
68+
'foo.cpp',
69+
'cpp'
70+
);
71+
await documentManager.showTextDocument(document);
72+
await tensorBoardImportTracker.activate();
73+
onDidImportTensorBoardListener.never().verify();
74+
});
75+
test('Ignore docstrings', async () => {
76+
const document = documentManager.addDocument(
77+
`"""
78+
import tensorboard
79+
"""`,
80+
'foo.py'
81+
);
82+
await documentManager.showTextDocument(document);
83+
await tensorBoardImportTracker.activate();
84+
onDidImportTensorBoardListener.never().verify();
85+
});
86+
});

0 commit comments

Comments
 (0)