-
Notifications
You must be signed in to change notification settings - Fork 77
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
python: detect app framework in .py files (#4625)
- creates context key for is/is not app - "Run App in Terminal" will show up when in an app We might want to think about how this interacts with the Shiny extension. I guess at this point, it will just populate a LOT of options in the Play button, which is ugly but benign. ### QA Notes <!-- Add additional information for QA on how to validate the change, paying special attention to the level of risk, adjacent areas that could be affected by the change, and any important contextual information not present in the linked issues. --> create a `.py` file ``` import streamlit as st x = st.slider('x') # 👈 this is a widget st.write(x, 'squared is', x * x) ``` open and click on play button, you should see ![Screenshot 2024-09-12 at 4 01 23 PM](https://github.com/user-attachments/assets/afacee31-e6cc-48d8-b185-1adcfd3bf95d) (clicking on this file will only save the file and have no other affect) --------- Signed-off-by: Isabel Zimmerman <[email protected]> Co-authored-by: Wasim Lorgat <[email protected]>
- Loading branch information
1 parent
73a9b78
commit 6af852f
Showing
10 changed files
with
308 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
91 changes: 91 additions & 0 deletions
91
extensions/positron-python/src/client/positron/webAppContexts.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
/*--------------------------------------------------------------------------------------------- | ||
* Copyright (C) 2024 Posit Software, PBC. All rights reserved. | ||
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information. | ||
*--------------------------------------------------------------------------------------------*/ | ||
|
||
import * as vscode from 'vscode'; | ||
import { executeCommand } from '../common/vscodeApis/commandApis'; | ||
|
||
const libraries: string[] = ['streamlit', 'shiny', 'dash', 'gradio', 'flask', 'fastapi']; | ||
|
||
export function detectWebApp(document: vscode.TextDocument): void { | ||
const text = document.getText(); | ||
const framework = getFramework(text); | ||
executeCommand('setContext', 'pythonAppFramework', framework); | ||
} | ||
|
||
export function getFramework(text: string): string | undefined { | ||
const importPattern = new RegExp(`import\\s+(${libraries.join('|')})`, 'g'); | ||
const fromImportPattern = new RegExp(`from\\s+(${libraries.join('|')})\\S*\\simport`, 'g'); | ||
const importMatch = importPattern.exec(text); | ||
|
||
if (importMatch) { | ||
return importMatch[1]; | ||
} | ||
|
||
const fromImportMatch = fromImportPattern.exec(text); | ||
if (fromImportMatch) { | ||
return fromImportMatch[1]; | ||
} | ||
|
||
return undefined; | ||
} | ||
|
||
export function activateAppDetection(disposables: vscode.Disposable[]): void { | ||
let timeout: NodeJS.Timeout | undefined; | ||
let activeEditor = vscode.window.activeTextEditor; | ||
|
||
function updateWebApp() { | ||
if (!activeEditor) { | ||
return; | ||
} | ||
detectWebApp(activeEditor.document); | ||
} | ||
|
||
// Throttle updates if needed | ||
function triggerUpdateApp(throttle = false) { | ||
if (!activeEditor) { | ||
return; | ||
} | ||
if (timeout) { | ||
clearTimeout(timeout); | ||
timeout = undefined; | ||
} | ||
if (throttle) { | ||
timeout = setTimeout(updateWebApp, 500); | ||
} else { | ||
detectWebApp(activeEditor.document); | ||
} | ||
} | ||
|
||
// Trigger for the current active editor. | ||
if (activeEditor) { | ||
triggerUpdateApp(); | ||
} | ||
|
||
disposables.push( | ||
// Trigger when the active editor changes | ||
vscode.window.onDidChangeActiveTextEditor((editor) => { | ||
if (editor && editor.document.languageId === 'python') { | ||
activeEditor = editor; | ||
triggerUpdateApp(); | ||
} | ||
}), | ||
|
||
// Trigger when the active editor's content changes | ||
vscode.workspace.onDidChangeTextDocument((event) => { | ||
if (activeEditor && event.document === activeEditor.document) { | ||
triggerUpdateApp(true); | ||
} | ||
}), | ||
|
||
// Trigger when new text document is opened | ||
vscode.workspace.onDidOpenTextDocument((document) => { | ||
if (document.languageId === 'python') { | ||
// update to opened text document | ||
activeEditor = vscode.window.activeTextEditor; | ||
triggerUpdateApp(); | ||
} | ||
}), | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
62 changes: 62 additions & 0 deletions
62
extensions/positron-python/src/test/positron/webAppContexts.unit.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import * as vscode from 'vscode'; | ||
import * as sinon from 'sinon'; | ||
import { assert } from 'chai'; | ||
import * as cmdApis from '../../client/common/vscodeApis/commandApis'; | ||
import { detectWebApp, getFramework } from '../../client/positron/webAppContexts'; | ||
import { IDisposableRegistry } from '../../client/common/types'; | ||
|
||
suite('Discover webapp frameworks', () => { | ||
let document: vscode.TextDocument; | ||
let executeCommandStub: sinon.SinonStub; | ||
const disposables: IDisposableRegistry = []; | ||
|
||
setup(() => { | ||
executeCommandStub = sinon.stub(cmdApis, 'executeCommand'); | ||
document = { | ||
getText: () => '', | ||
} as vscode.TextDocument; | ||
}); | ||
|
||
teardown(() => { | ||
sinon.restore(); | ||
disposables.forEach((d) => d.dispose()); | ||
}); | ||
|
||
const texts = { | ||
'import streamlit': 'streamlit', | ||
'from shiny.ui import page_navbar': 'shiny', | ||
'import numpy': 'numpy', | ||
}; | ||
Object.entries(texts).forEach(([text, framework]) => { | ||
const expected = text.includes('numpy') ? undefined : framework; | ||
test('should set context pythonAppFramework if application is found', () => { | ||
document.getText = () => text; | ||
detectWebApp(document); | ||
|
||
assert.ok(executeCommandStub.calledOnceWith('setContext', 'pythonAppFramework', expected)); | ||
}); | ||
}); | ||
|
||
const frameworks = ['streamlit', 'shiny', 'gradio', 'flask', 'fastapi', 'numpy']; | ||
frameworks.forEach((framework) => { | ||
const expected = framework === 'numpy' ? undefined : framework; | ||
test(`should detect ${expected}: import framework`, () => { | ||
const text = `import ${framework}`; | ||
const actual = getFramework(text); | ||
|
||
assert.strictEqual(actual, expected); | ||
}); | ||
test(`should detect ${expected}: from framework.test import XYZ`, () => { | ||
const text = `from ${framework}.test import XYZ`; | ||
const actual = getFramework(text); | ||
|
||
assert.strictEqual(actual, expected); | ||
}); | ||
test(`should detect ${expected}: from framework import XYZ`, () => { | ||
const text = `from ${framework} import XYZ`; | ||
const actual = getFramework(text); | ||
|
||
assert.strictEqual(actual, expected); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.