Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft: Generic "Run app" framework #4616

Closed
wants to merge 13 commits into from
30 changes: 30 additions & 0 deletions extensions/positron-python/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,36 @@
"category": "Python",
"command": "python.installJupyter",
"title": "%python.command.python.installJupyter.title%"
},
{
"category": "Python",
"command": "python.runShinyApp",
"title": "%python.command.python.runShinyApp.title%"
},
{
"category": "Python",
"command": "python.runStreamlitApp",
"title": "%python.command.python.runStreamlitApp.title%"
},
{
"category": "Python",
"command": "python.runDashApp",
"title": "%python.command.python.runDashApp.title%"
},
{
"category": "Python",
"command": "python.runGradioApp",
"title": "%python.command.python.runGradioApp.title%"
},
{
"category": "Python",
"command": "python.runFastAPIApp",
"title": "%python.command.python.runFastAPIApp.title%"
},
{
"category": "Python",
"command": "python.runFlaskApp",
"title": "%python.command.python.runFlaskApp.title%"
}
],
"configuration": {
Expand Down
6 changes: 6 additions & 0 deletions extensions/positron-python/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@
"python.command.python.clearWorkspaceInterpreter.title": "Clear Workspace Interpreter Setting",
"python.command.python.viewOutput.title": "Show Output",
"python.command.python.installJupyter.title": "Install the Jupyter extension",
"python.command.python.runShinyApp.title": "Run Shiny App",
"python.command.python.runStreamlitApp.title": "Run Streamlit App",
"python.command.python.runDashApp.title": "Run Dash App",
"python.command.python.runGradioApp.title": "Run Gradio App",
"python.command.python.runFastAPIApp.title": "Run FastAPI App",
"python.command.python.runFlaskApp.title": "Run Flask App",
"python.command.python.viewLanguageServerOutput.title": "Show Language Server Output",
"python.command.python.configureTests.title": "Configure Tests",
"python.command.testing.rerunFailedTests.title": "Rerun Failed Tests",
Expand Down
148 changes: 148 additions & 0 deletions extensions/positron-python/src/client/positron/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/

// eslint-disable-next-line import/no-unresolved
import * as positron from 'positron';
import * as path from 'path';
import * as vscode from 'vscode';
import { ProgressLocation, ProgressOptions } from 'vscode';
import * as fs from 'fs';
Expand Down Expand Up @@ -71,6 +74,135 @@ export async function activatePositron(serviceContainer: IServiceContainer): Pro
disposables.push(
vscode.commands.registerCommand('python.getMinimumPythonVersion', (): string => MINIMUM_PYTHON_VERSION.raw),
);
// Register application runners.
disposables.push(
positron.applications.registerApplicationRunner('python.shiny', {
label: 'Shiny',
languageId: 'python',
getRunOptions(runtimePath, document, port) {
return {
command: [
runtimePath,
'-m',
'shiny',
'run',
'--port',
port.toString(),
'--reload',
// TODO: --autoreload-port
document.uri.fsPath,
].join(' '),
};
},
}),
positron.applications.registerApplicationRunner('python.streamlit', {
label: 'Streamlit',
languageId: 'python',
getRunOptions(runtimePath, document, port) {
return {
command: [
runtimePath,
'-m',
'streamlit',
'run',
document.uri.fsPath,
'--server.port',
port.toString(),
'--server.headless',
'true',
].join(' '),
};
},
}),
positron.applications.registerApplicationRunner('python.dash', {
label: 'Dash',
languageId: 'python',
getRunOptions(runtimePath, document, port) {
return {
command: [runtimePath, document.uri.fsPath].join(' '),
env: {
PORT: port.toString(),
},
};
},
}),
positron.applications.registerApplicationRunner('python.gradio', {
label: 'Gradio',
languageId: 'python',
getRunOptions(runtimePath, document, port) {
return {
command: [runtimePath, document.uri.fsPath].join(' '),
env: {
GRADIO_SERVER_PORT: port.toString(),
},
};
},
}),
positron.applications.registerApplicationRunner('python.fastapi', {
label: 'FastAPI',
languageId: 'python',
getRunOptions(runtimePath, document, port) {
const text = document.getText();
const appName = getAppName(text, 'FastAPI');
if (!appName) {
throw new Error('No FastAPI app object found');
}
return {
command: [
runtimePath,
'-m',
'uvicorn',
`${pathToModule(document.uri.fsPath)}:${appName}`,
'--port',
port.toString(),
].join(' '),
url: `http://localhost:${port}/docs`,
};
},
}),
positron.applications.registerApplicationRunner('python.flask', {
label: 'Flask',
languageId: 'python',
getRunOptions(runtimePath, document, port) {
const text = document.getText();
const appName = getAppName(text, 'Flask');
if (!appName) {
throw new Error('No Flask app object found');
}
return {
command: [
runtimePath,
'-m',
'flask',
'--app',
`${pathToModule(document.uri.fsPath)}:${appName}`,
'run',
'--port',
port.toString(),
].join(' '),
};
},
}),

vscode.commands.registerCommand('python.runShinyApp', async () => {
await positron.applications.runApplication('python.shiny');
}),
vscode.commands.registerCommand('python.runStreamlitApp', async () => {
await positron.applications.runApplication('python.streamlit');
}),
vscode.commands.registerCommand('python.runDashApp', async () => {
await positron.applications.runApplication('python.dash');
}),
vscode.commands.registerCommand('python.runGradioApp', async () => {
await positron.applications.runApplication('python.gradio');
}),
vscode.commands.registerCommand('python.runFastAPIApp', async () => {
await positron.applications.runApplication('python.fastapi');
}),
vscode.commands.registerCommand('python.runFlaskApp', async () => {
await positron.applications.runApplication('python.flask');
}),
);
traceInfo('activatePositron: done!');
} catch (ex) {
traceError('activatePositron() failed.', ex);
Expand Down Expand Up @@ -113,3 +245,19 @@ export async function checkAndInstallPython(
}
return InstallerResponse.Installed;
}

// TODO: Better way?
function pathToModule(p: string): string {
const workspacePath = vscode.workspace.workspaceFolders?.[0].uri.fsPath;
if (!workspacePath) {
throw new Error('No workspace path');
}
const relativePath = path.relative(workspacePath, p);
const mod = path.parse(relativePath).name;
const parts = path.dirname(relativePath).split(path.sep);
return parts.concat(mod).join('.');
}

function getAppName(text: string, className: string): string | undefined {
return text.match(new RegExp(`([^\\s]+)\\s*=\\s*${className}\\(`))?.[1];
}
20 changes: 20 additions & 0 deletions src/positron-dts/positron.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1070,6 +1070,18 @@ declare module 'positron' {
pasteText(text: string): void;
}

export interface ApplicationRunOptions {
command: string;
env?: { [key: string]: string | null | undefined };
url?: string;
}

export interface ApplicationRunner {
label: string;
languageId: string;
getRunOptions(runtimePath: string, document: vscode.TextDocument, port: number): ApplicationRunOptions;
}

namespace languages {
/**
* Register a statement range provider.
Expand Down Expand Up @@ -1353,4 +1365,12 @@ declare module 'positron' {


}

namespace applications {

export function registerApplicationRunner(id: string, runner: ApplicationRunner): vscode.Disposable;

export function runApplication(id: string): Thenable<void>;

}
}
33 changes: 33 additions & 0 deletions src/vs/base/common/async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1410,6 +1410,39 @@ export async function retry<T>(task: ITask<Promise<T>>, delay: number, retries:
throw lastError;
}

// --- Start Positron ---
/**
* Retry a task until it succeeds or times out.
*
* @param task The task to run.
* @param delay The delay between retries in milliseconds.
* @param timeout Stop retrying after this number of milliseconds.
* @returns Promise that resolves with the result of the task, or rejects with the last error.
*/
export async function retryTimeout<T>(task: ITask<Promise<T>>, delay: number, timeout: number): Promise<T> {
// Track whether the task timed out.
let timedOut = false;
const timer = setTimeout(() => timedOut = true, timeout);

while (true) {
try {
// Run the task and clear the timer if it completes.
const result = await task();
clearTimeout(timer);
return result;
} catch (error) {
// If we timed out, throw the error.
if (timedOut) {
throw error;
}

// Otherwise, wait for the delay and try again.
await new Promise<void>((resolve) => setTimeout(() => resolve(), delay));
}
}
}
// --- End Positron ---

//#region Task Sequentializer

interface IRunningTask {
Expand Down
40 changes: 40 additions & 0 deletions src/vs/base/node/ports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
*--------------------------------------------------------------------------------------------*/

import * as net from 'net';
// --- Start Positron ---
import { raceTimeout, retryTimeout } from 'vs/base/common/async';
// --- End Positron ---

/**
* Given a start point and a max number of retries, will find a port that
Expand Down Expand Up @@ -201,3 +204,40 @@ function dispose(socket: net.Socket): void {
console.error(error); // otherwise this error would get lost in the callback chain
}
}

// --- Start Positron ---
/**
* Wait for a port on localhost to be ready for a connection.
*
* @param port The port on localhost.
* @param timeout Stop retrying after this number of milliseconds.
* @returns A promise that resolves when the port is ready for a connection,
* or rejects if the timeout is reached.
*/
export async function waitForPortConnection(port: number, timeout: number): Promise<void> {
// Retry connecting to the port until it is ready.
return retryTimeout(() => {
// Also apply a timeout to the connection attempt.
return raceTimeout(
// Create a promise that resolves when the port is ready for a connection,
// or rejects if the connection attempt fails.
new Promise((resolve, reject) => {
const client = new net.Socket();

// If we can connect to the port, resolve the promise.
client.once('connect', () => {
dispose(client);
resolve();
});

// If we can't connect to the port, reject the promise.
client.once('error', (err) => {
dispose(client);
reject(err);
});

client.connect(port, '127.0.0.1');
}), 5000);
}, 50, timeout);
}
// --- End Positron ---
Loading
Loading