Skip to content

Commit 1791011

Browse files
jmcpherswesm
authored andcommitted
Merged PR posit-dev/positron-python#101: Wrap Jupyter Kernel runtime with Python Language Runtime
Merge pull request #101 from posit-dev/feature/jupyter-adapter-api Wrap Jupyter Kernel runtime with Python Language Runtime -------------------- Commit message for posit-dev/positron-python@ca3b312: Merge remote-tracking branch 'origin/main' into feature/jupyter-adapter-api -------------------- Commit message for posit-dev/positron-python@8dd9758: allow unresolved 'positron' import (TS lint) -------------------- Commit message for posit-dev/positron-python@d446de6: implement shutdown/restart/interrupt as async methods -------------------- Commit message for posit-dev/positron-python@08e9e26: register new PythonLanguageRuntime wrapping the kernel -------------------- Commit message for posit-dev/positron-python@5f66208: add wrapper class for language runtime kernel -------------------- Commit message for posit-dev/positron-python@e0190c5: add jupyter adapter types Authored-by: Jonathan McPherson <[email protected]> Signed-off-by: Jonathan McPherson <[email protected]>
1 parent 1a71201 commit 1791011

File tree

4 files changed

+247
-22
lines changed

4 files changed

+247
-22
lines changed

extensions/positron-python/positron-dts/positron.d.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ declare module 'positron' {
8585
/** The runtime is busy executing code. */
8686
Busy = 'busy',
8787

88+
/** The runtime is in the process of restarting. */
89+
Restarting = 'restarting',
90+
8891
/** The runtime is in the process of shutting down. */
8992
Exiting = 'exiting',
9093

@@ -314,15 +317,15 @@ declare module 'positron' {
314317
/** The version of the language; e.g. "4.2" */
315318
languageVersion: string;
316319

317-
/** The Base64-encoded icon SVG for the language. */
318-
base64EncodedIconSvg: string | undefined;
319-
320320
/** The text the language's interpreter uses to prompt the user for input, e.g. ">" or ">>>" */
321321
inputPrompt: string;
322322

323323
/** The text the language's interpreter uses to prompt the user for continued input, e.g. "+" or "..." */
324324
continuationPrompt: string;
325325

326+
/** The Base64-encoded icon SVG for the language. */
327+
base64EncodedIconSvg: string | undefined;
328+
326329
/** Whether the runtime should start up automatically or wait until explicitly requested */
327330
startupBehavior: LanguageRuntimeStartupBehavior;
328331
}
@@ -473,14 +476,25 @@ declare module 'positron' {
473476
*/
474477
start(): Thenable<LanguageRuntimeInfo>;
475478

476-
/** Interrupt the runtime */
477-
interrupt(): void;
479+
/**
480+
* Interrupt the runtime; returns a Thenable that resolves when the interrupt has been
481+
* successfully sent to the runtime (not necessarily when it has been processed)
482+
*/
483+
interrupt(): Thenable<void>;
478484

479-
/** Restart the runtime */
480-
restart(): void;
485+
/**
486+
* Restart the runtime; returns a Thenable that resolves when the runtime restart sequence
487+
* has been successfully started (not necessarily when it has completed). A restart will
488+
* cause the runtime to be shut down and then started again; its status will change from
489+
* `Restarting` => `Exited` => `Initializing` => `Starting` => `Ready`.
490+
*/
491+
restart(): Thenable<void>;
481492

482-
/** Shut down the runtime */
483-
shutdown(): void;
493+
/**
494+
* Shut down the runtime; returns a Thenable that resolves when the runtime shutdown
495+
* sequence has been successfully started (not necessarily when it has completed).
496+
*/
497+
shutdown(): Thenable<void>;
484498
}
485499

486500

extensions/positron-python/src/client/activation/jedi/positronLanguageRuntimes.ts

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import * as fs from 'fs';
1111
// eslint-disable-next-line import/no-unresolved
1212
import * as positron from 'positron';
1313
import * as vscode from 'vscode';
14+
import * as crypto from 'crypto';
1415
import { Disposable, DocumentFilter, LanguageClient, LanguageClientOptions, ServerOptions, StreamInfo } from 'vscode-languageclient/node';
1516

1617
import { compare } from 'semver';
@@ -24,6 +25,8 @@ import { PythonEnvironment } from '../../pythonEnvironments/info';
2425
import { PythonVersion } from '../../pythonEnvironments/info/pythonVersion';
2526
import { ProgressReporting } from '../progress';
2627
import { ILanguageServerProxy } from '../types';
28+
import { PythonLanguageRuntime } from './pythonLanguageRuntime';
29+
import { JupyterAdapterApi } from '../../jupyter-adapter.d'
2730

2831
// Load the Python icon.
2932
const base64EncodedIconSvg = fs.readFileSync(path.join(EXTENSION_ROOT_DIR, 'resources', 'branding', 'python-icon.svg')).toString('base64');
@@ -97,7 +100,11 @@ export class PositronJediLanguageServerProxy implements ILanguageServerProxy {
97100

98101
// Register available python interpreters as language runtimes with our Jupyter Adapter
99102
this.withActiveExtention(ext, async () => {
100-
await this.registerLanguageRuntimes(ext, resource, interpreter, options);
103+
// Create typesafe reference to the Jupyter Adapter's API
104+
const jupyterAdapterApi = ext.exports as JupyterAdapterApi;
105+
106+
// Register the language runtimes for each available interpreter
107+
await this.registerLanguageRuntimes(jupyterAdapterApi, resource, interpreter, options);
101108
});
102109
}
103110

@@ -139,7 +146,7 @@ export class PositronJediLanguageServerProxy implements ILanguageServerProxy {
139146
* Register available python environments as a language runtime with the Jupyter Adapter.
140147
*/
141148
private async registerLanguageRuntimes(
142-
ext: vscode.Extension<any>,
149+
jupyterAdapterApi: JupyterAdapterApi,
143150
resource: Resource,
144151
preferredInterpreter: PythonEnvironment | undefined,
145152
options: LanguageClientOptions
@@ -167,7 +174,8 @@ export class PositronJediLanguageServerProxy implements ILanguageServerProxy {
167174
debugPort = await portfinder.getPortPromise({ port: debugPort });
168175
}
169176

170-
const runtime: vscode.Disposable = await this.registerLanguageRuntime(ext, interpreter, debugPort, logLevel, options);
177+
const runtime: vscode.Disposable = await this.registerLanguageRuntime(
178+
jupyterAdapterApi, interpreter, debugPort, logLevel, options);
171179
this.disposables.push(runtime);
172180

173181
if (debugPort !== undefined) {
@@ -182,7 +190,7 @@ export class PositronJediLanguageServerProxy implements ILanguageServerProxy {
182190
* IPyKernel with a connection file.
183191
*/
184192
private async registerLanguageRuntime(
185-
ext: vscode.Extension<any>,
193+
jupyterAdapterApi: JupyterAdapterApi,
186194
interpreter: PythonEnvironment,
187195
debugPort: number | undefined,
188196
logLevel: string,
@@ -210,16 +218,40 @@ export class PositronJediLanguageServerProxy implements ILanguageServerProxy {
210218
};
211219
traceVerbose(`Configuring Jedi LSP with IPyKernel using args '${args}'`);
212220

213-
// Create an adapter for the kernel as our language runtime
214-
const runtime: positron.LanguageRuntime = ext.exports.adaptKernel(kernelSpec,
215-
PYTHON_LANGUAGE,
216-
pythonVersion,
217-
this.extensionVersion,
221+
// Create a stable ID for the runtime based on the interpreter path and version.
222+
const digest = crypto.createHash('sha256');
223+
digest.update(JSON.stringify(kernelSpec));
224+
digest.update(pythonVersion ?? '');
225+
const runtimeId = digest.digest('hex').substring(0, 32);
226+
227+
// Create the metadata for the language runtime
228+
const metadata: positron.LanguageRuntimeMetadata = {
229+
runtimePath: interpreter.path,
230+
runtimeId,
231+
runtimeName: displayName,
232+
runtimeVersion: this.extensionVersion ?? '0.0.0',
233+
languageName: kernelSpec.language,
234+
languageId: PYTHON_LANGUAGE,
235+
languageVersion: pythonVersion ?? '0.0.0',
218236
base64EncodedIconSvg,
219-
'>>>',
220-
'...',
221-
startupBehavior,
222-
(port: number) => this.startClient(options, port));
237+
inputPrompt: '>>>',
238+
continuationPrompt: '...',
239+
startupBehavior
240+
}
241+
242+
// Create an adapter for the kernel as our language runtime
243+
const runtime = new PythonLanguageRuntime(
244+
kernelSpec, metadata, jupyterAdapterApi);
245+
246+
runtime.onDidChangeRuntimeState((state) => {
247+
if (state === positron.RuntimeState.Ready) {
248+
jupyterAdapterApi.findAvailablePort([], 25).then((port: number) => {
249+
runtime.emitJupyterLog(`Starting Positron LSP server on port ${port}`);
250+
runtime.startPositronLsp(`127.0.0.1:${port}`);
251+
this.startClient(options, port).ignoreErrors();
252+
});
253+
}
254+
});
223255

224256
// Register our language runtime provider
225257
return positron.runtime.registerLanguageRuntime(runtime);
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (C) 2023 Posit Software, PBC. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
// eslint-disable-next-line import/no-unresolved
6+
import * as positron from 'positron';
7+
import * as vscode from 'vscode';
8+
import { JupyterAdapterApi, JupyterKernelSpec, JupyterLanguageRuntime } from '../../jupyter-adapter.d';
9+
10+
/**
11+
* A Positron Python language runtime that wraps a Jupyter kernel runtime.
12+
* Fulfills most Language Runtime API calls by delegating to the wrapped kernel.
13+
*/
14+
export class PythonLanguageRuntime implements JupyterLanguageRuntime {
15+
16+
/** The Jupyter kernel-based implementation of the Language Runtime API */
17+
private _kernel: JupyterLanguageRuntime;
18+
19+
/**
20+
* Create a new PythonLanguageRuntime object to wrap a Jupyter kernel.
21+
*
22+
* @param kernelSpec The specification of the Jupyter kernel to wrap.
23+
* @param metadata The metadata of the language runtime to create.
24+
* @param adapterApi The API of the Jupyter Adapter extension.
25+
*/
26+
constructor(
27+
readonly kernelSpec: JupyterKernelSpec,
28+
readonly metadata: positron.LanguageRuntimeMetadata,
29+
readonly adapterApi: JupyterAdapterApi) {
30+
31+
this._kernel = adapterApi.adaptKernel(kernelSpec, metadata);
32+
33+
this.onDidChangeRuntimeState = this._kernel.onDidChangeRuntimeState;
34+
this.onDidReceiveRuntimeMessage = this._kernel.onDidReceiveRuntimeMessage;
35+
}
36+
37+
startPositronLsp(clientAddress: string): void {
38+
this._kernel.startPositronLsp(clientAddress);
39+
}
40+
41+
emitJupyterLog(message: string): void {
42+
this._kernel.emitJupyterLog(message);
43+
}
44+
45+
onDidReceiveRuntimeMessage: vscode.Event<positron.LanguageRuntimeMessage>;
46+
47+
onDidChangeRuntimeState: vscode.Event<positron.RuntimeState>;
48+
49+
execute(code: string, id: string, mode: positron.RuntimeCodeExecutionMode, errorBehavior: positron.RuntimeErrorBehavior): void {
50+
this._kernel.execute(code, id, mode, errorBehavior);
51+
}
52+
53+
isCodeFragmentComplete(code: string): Thenable<positron.RuntimeCodeFragmentStatus> {
54+
return this._kernel.isCodeFragmentComplete(code);
55+
}
56+
57+
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
58+
createClient(id: string, type: positron.RuntimeClientType, params: any): Thenable<void> {
59+
return this._kernel.createClient(id, type, params);
60+
}
61+
62+
listClients(type?: positron.RuntimeClientType | undefined): Thenable<Record<string, string>> {
63+
return this._kernel.listClients(type);
64+
}
65+
66+
removeClient(id: string): void {
67+
this._kernel.removeClient(id);
68+
}
69+
70+
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
71+
sendClientMessage(clientId: string, messageId: string, message: any): void {
72+
this._kernel.sendClientMessage(clientId, messageId, message);
73+
}
74+
75+
replyToPrompt(id: string, reply: string): void {
76+
this._kernel.replyToPrompt(id, reply);
77+
}
78+
79+
start(): Thenable<positron.LanguageRuntimeInfo> {
80+
return this._kernel.start();
81+
}
82+
83+
async interrupt(): Promise<void> {
84+
return this._kernel.interrupt();
85+
}
86+
87+
async restart(): Promise<void> {
88+
return this._kernel.restart();
89+
}
90+
91+
async shutdown(): Promise<void> {
92+
return this._kernel.shutdown();
93+
}
94+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (C) 2023 Posit Software, PBC. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
import * as vscode from 'vscode';
6+
7+
// eslint-disable-next-line import/no-unresolved
8+
import * as positron from 'positron';
9+
10+
/**
11+
* This set of type definitions defines the interfaces used by the Positron
12+
* Jupyter Adapter extension.
13+
*/
14+
15+
/**
16+
* Represents a registered Jupyter Kernel. These types are defined in the
17+
* Jupyter documentation at:
18+
*
19+
* https://jupyter-client.readthedocs.io/en/stable/kernels.html#kernel-specs
20+
*/
21+
export interface JupyterKernelSpec {
22+
/** Command used to start the kernel and an array of command line arguments */
23+
argv: Array<string>;
24+
25+
/** The kernel's display name */
26+
display_name: string; // eslint-disable-line
27+
28+
/** The language the kernel executes */
29+
language: string;
30+
31+
/** Interrupt mode (signal or message) */
32+
interrupt_mode?: 'signal' | 'message'; // eslint-disable-line
33+
34+
/** Environment variables to set when starting the kernel */
35+
env?: { [key: string]: string };
36+
}
37+
38+
/**
39+
* A language runtime that wraps a Jupyter kernel.
40+
*/
41+
export interface JupyterLanguageRuntime extends positron.LanguageRuntime {
42+
/**
43+
* Convenience method for starting the Positron LSP server, if the
44+
* language runtime supports it.
45+
*
46+
* @param clientAddress The address of the client that will connect to the
47+
* language server.
48+
*/
49+
startPositronLsp(clientAddress: string): void;
50+
51+
/**
52+
* Method for emitting a message to the language server's Jupyter output
53+
* channel.
54+
*
55+
* @param message A message to emit to the Jupyter log.
56+
*/
57+
emitJupyterLog(message: string): void;
58+
}
59+
60+
/**
61+
* The Jupyter Adapter API as exposed by the Jupyter Adapter extension.
62+
*/
63+
export interface JupyterAdapterApi extends vscode.Disposable {
64+
65+
/**
66+
* Create an adapter for a Jupyter-compatible kernel.
67+
*
68+
* @param kernel A Jupyter kernel spec containing the information needed to
69+
* start the kernel.
70+
* @param metadata The metadata for the language runtime to be wrapped by the
71+
* adapter.
72+
* @returns A LanguageRuntimeAdapter that wraps the kernel.
73+
*/
74+
adaptKernel(kernel: JupyterKernelSpec,
75+
metadata: positron.LanguageRuntimeMetadata): JupyterLanguageRuntime;
76+
77+
/**
78+
* Finds an available TCP port for a server
79+
*
80+
* @param excluding A list of ports to exclude from the search
81+
* @param maxTries The maximum number of attempts
82+
* @returns An available TCP port
83+
*/
84+
findAvailablePort(excluding: Array<number>, maxTries: number): Promise<number>;
85+
}

0 commit comments

Comments
 (0)