Skip to content

Commit b04e0d8

Browse files
Kartik Rajkarthiknadig
authored andcommitted
Finalizing design of proposed API for python environments (microsoft/vscode-python#19841)
Closes microsoft/vscode-python#19101 closes #18973 Co-authored-by: Karthik Nadig <[email protected]>
1 parent 1d327e5 commit b04e0d8

21 files changed

+1309
-592
lines changed

extensions/positron-python/.eslintignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,6 @@ src/client/common/application/languageService.ts
246246
src/client/common/application/clipboard.ts
247247
src/client/common/application/workspace.ts
248248
src/client/common/application/debugSessionTelemetry.ts
249-
src/client/common/application/extensions.ts
250249
src/client/common/application/documentManager.ts
251250
src/client/common/application/debugService.ts
252251
src/client/common/application/commands/reloadCommand.ts

extensions/positron-python/package-lock.json

Lines changed: 15 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

extensions/positron-python/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1816,6 +1816,7 @@
18161816
"request-progress": "^3.0.0",
18171817
"rxjs": "^6.5.4",
18181818
"rxjs-compat": "^6.5.4",
1819+
"stack-trace": "0.0.10",
18191820
"semver": "^5.5.0",
18201821
"sudo-prompt": "^9.2.1",
18211822
"tmp": "^0.0.33",
@@ -1852,11 +1853,12 @@
18521853
"@types/semver": "^5.5.0",
18531854
"@types/shortid": "^0.0.29",
18541855
"@types/sinon": "^10.0.11",
1856+
"@types/stack-trace": "0.0.29",
18551857
"@types/tmp": "^0.0.33",
18561858
"@types/uuid": "^8.3.4",
18571859
"@types/vscode": "~1.68.0",
1858-
"@types/winreg": "^1.2.30",
18591860
"@types/which": "^2.0.1",
1861+
"@types/winreg": "^1.2.30",
18601862
"@types/xml2js": "^0.4.2",
18611863
"@typescript-eslint/eslint-plugin": "^3.7.0",
18621864
"@typescript-eslint/parser": "^3.7.0",

extensions/positron-python/src/client/apiTypes.ts

Lines changed: 0 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
import { Event, Uri } from 'vscode';
55
import { Resource } from './common/types';
66
import { IDataViewerDataProvider, IJupyterUriProvider } from './jupyter/types';
7-
import { EnvPathType, PythonEnvKind } from './pythonEnvironments/base/info';
8-
import { GetRefreshEnvironmentsOptions, ProgressNotificationEvent } from './pythonEnvironments/base/locator';
97

108
/*
119
* Do not introduce any breaking changes to this API.
@@ -88,137 +86,3 @@ export interface IExtensionApi {
8886
registerRemoteServerProvider(serverProvider: IJupyterUriProvider): void;
8987
};
9088
}
91-
92-
export interface EnvironmentDetailsOptions {
93-
useCache: boolean;
94-
}
95-
96-
export interface EnvironmentDetails {
97-
interpreterPath: string;
98-
envFolderPath?: string;
99-
version: string[];
100-
environmentType: PythonEnvKind[];
101-
metadata: Record<string, unknown>;
102-
}
103-
104-
export interface EnvironmentsChangedParams {
105-
/**
106-
* Path to environment folder or path to interpreter that uniquely identifies an environment.
107-
* Virtual environments lacking an interpreter are identified by environment folder paths,
108-
* whereas other envs can be identified using interpreter path.
109-
*/
110-
path?: string;
111-
type: 'add' | 'remove' | 'update' | 'clear-all';
112-
}
113-
114-
export interface ActiveEnvironmentChangedParams {
115-
/**
116-
* Path to environment folder or path to interpreter that uniquely identifies an environment.
117-
* Virtual environments lacking an interpreter are identified by environment folder paths,
118-
* whereas other envs can be identified using interpreter path.
119-
*/
120-
path: string;
121-
resource?: Uri;
122-
}
123-
124-
export interface IProposedExtensionAPI {
125-
environment: {
126-
/**
127-
* An event that is emitted when execution details (for a resource) change. For instance, when interpreter configuration changes.
128-
*/
129-
readonly onDidChangeExecutionDetails: Event<Uri | undefined>;
130-
/**
131-
* Returns all the details the consumer needs to execute code within the selected environment,
132-
* corresponding to the specified resource taking into account any workspace-specific settings
133-
* for the workspace to which this resource belongs.
134-
* @param {Resource} [resource] A resource for which the setting is asked for.
135-
* * When no resource is provided, the setting scoped to the first workspace folder is returned.
136-
* * If no folder is present, it returns the global setting.
137-
* @returns {({ execCommand: string[] | undefined })}
138-
*/
139-
getExecutionDetails(
140-
resource?: Resource,
141-
): Promise<{
142-
/**
143-
* E.g of execution commands returned could be,
144-
* * `['<path to the interpreter set in settings>']`
145-
* * `['<path to the interpreter selected by the extension when setting is not set>']`
146-
* * `['conda', 'run', 'python']` which is used to run from within Conda environments.
147-
* or something similar for some other Python environments.
148-
*
149-
* @type {(string[] | undefined)} When return value is `undefined`, it means no interpreter is set.
150-
* Otherwise, join the items returned using space to construct the full execution command.
151-
*/
152-
execCommand: string[] | undefined;
153-
}>;
154-
/**
155-
* Returns the path to the python binary selected by the user or as in the settings.
156-
* This is just the path to the python binary, this does not provide activation or any
157-
* other activation command. The `resource` if provided will be used to determine the
158-
* python binary in a multi-root scenario. If resource is `undefined` then the API
159-
* returns what ever is set for the workspace.
160-
* @param resource : Uri of a file or workspace
161-
*/
162-
getActiveEnvironmentPath(resource?: Resource): Promise<EnvPathType | undefined>;
163-
/**
164-
* Returns details for the given interpreter. Details such as absolute interpreter path,
165-
* version, type (conda, pyenv, etc). Metadata such as `sysPrefix` can be found under
166-
* metadata field.
167-
* @param path : Full path to environment folder or interpreter whose details you need.
168-
* @param options : [optional]
169-
* * useCache : When true, cache is checked first for any data, returns even if there
170-
* is partial data.
171-
*/
172-
getEnvironmentDetails(
173-
path: string,
174-
options?: EnvironmentDetailsOptions,
175-
): Promise<EnvironmentDetails | undefined>;
176-
/**
177-
* Returns paths to environments that uniquely identifies an environment found by the extension
178-
* at the time of calling. This API will *not* trigger a refresh. If a refresh is going on it
179-
* will *not* wait for the refresh to finish. This will return what is known so far. To get
180-
* complete list `await` on promise returned by `getRefreshPromise()`.
181-
*
182-
* Virtual environments lacking an interpreter are identified by environment folder paths,
183-
* whereas other envs can be identified using interpreter path.
184-
*/
185-
getEnvironmentPaths(): Promise<EnvPathType[] | undefined>;
186-
/**
187-
* Sets the active environment path for the python extension for the resource. Configuration target
188-
* will always be the workspace folder.
189-
* @param path : Full path to environment folder or interpreter to set.
190-
* @param resource : [optional] Uri of a file ro workspace to scope to a particular workspace
191-
* folder.
192-
*/
193-
setActiveEnvironment(path: string, resource?: Resource): Promise<void>;
194-
/**
195-
* This API will re-trigger environment discovery. Extensions can wait on the returned
196-
* promise to get the updated environment list. If there is a refresh already going on
197-
* then it returns the promise for that refresh.
198-
* @param options : [optional]
199-
* * clearCache : When true, this will clear the cache before environment refresh
200-
* is triggered.
201-
*/
202-
refreshEnvironment(): Promise<EnvPathType[] | undefined>;
203-
/**
204-
* Tracks discovery progress for current list of known environments, i.e when it starts, finishes or any other relevant
205-
* stage. Note the progress for a particular query is currently not tracked or reported, this only indicates progress of
206-
* the entire collection.
207-
*/
208-
readonly onRefreshProgress: Event<ProgressNotificationEvent>;
209-
/**
210-
* Returns a promise for the ongoing refresh. Returns `undefined` if there are no active
211-
* refreshes going on.
212-
*/
213-
getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise<void> | undefined;
214-
/**
215-
* This event is triggered when the known environment list changes, like when a environment
216-
* is found, existing environment is removed, or some details changed on an environment.
217-
*/
218-
onDidEnvironmentsChanged: Event<EnvironmentsChangedParams[]>;
219-
/**
220-
* This event is triggered when the active environment changes.
221-
*/
222-
onDidActiveEnvironmentChanged: Event<ActiveEnvironmentChangedParams>;
223-
};
224-
}
Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
1-
// Copyright (c) Microsoft Corporation. All rights reserved.
1+
/* eslint-disable class-methods-use-this */
2+
// Copyright (c) Microsoft Corporation.
23
// Licensed under the MIT License.
34

45
'use strict';
56

6-
import { injectable } from 'inversify';
7+
import { inject, injectable } from 'inversify';
78
import { Event, Extension, extensions } from 'vscode';
9+
import * as stacktrace from 'stack-trace';
10+
import * as path from 'path';
811
import { IExtensions } from '../types';
12+
import { IFileSystem } from '../platform/types';
13+
import { EXTENSION_ROOT_DIR } from '../constants';
914

15+
/**
16+
* Provides functions for tracking the list of extensions that VSCode has installed.
17+
*/
1018
@injectable()
1119
export class Extensions implements IExtensions {
20+
constructor(@inject(IFileSystem) private readonly fs: IFileSystem) {}
21+
22+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1223
public get all(): readonly Extension<any>[] {
1324
return extensions.all;
1425
}
@@ -17,7 +28,59 @@ export class Extensions implements IExtensions {
1728
return extensions.onDidChange;
1829
}
1930

20-
public getExtension(extensionId: any) {
31+
public getExtension(extensionId: string): Extension<unknown> | undefined {
2132
return extensions.getExtension(extensionId);
2233
}
34+
35+
/**
36+
* Code borrowed from:
37+
* https://github.com/microsoft/vscode-jupyter/blob/67fe33d072f11d6443cf232a06bed0ac5e24682c/src/platform/common/application/extensions.node.ts
38+
*/
39+
public async determineExtensionFromCallStack(): Promise<{ extensionId: string; displayName: string }> {
40+
const { stack } = new Error();
41+
if (stack) {
42+
const pythonExtRoot = path.join(EXTENSION_ROOT_DIR.toLowerCase(), path.sep);
43+
const frames = stack
44+
.split('\n')
45+
.map((f) => {
46+
const result = /\((.*)\)/.exec(f);
47+
if (result) {
48+
return result[1];
49+
}
50+
return undefined;
51+
})
52+
.filter((item) => item && !item.toLowerCase().startsWith(pythonExtRoot))
53+
.filter((item) =>
54+
this.all.some(
55+
(ext) => item!.includes(ext.extensionUri.path) || item!.includes(ext.extensionUri.fsPath),
56+
),
57+
) as string[];
58+
stacktrace.parse(new Error('Ex')).forEach((item) => {
59+
const fileName = item.getFileName();
60+
if (fileName && !fileName.toLowerCase().startsWith(pythonExtRoot)) {
61+
frames.push(fileName);
62+
}
63+
});
64+
for (const frame of frames) {
65+
// This file is from a different extension. Try to find its `package.json`.
66+
let dirName = path.dirname(frame);
67+
let last = frame;
68+
while (dirName && dirName.length < last.length) {
69+
const possiblePackageJson = path.join(dirName, 'package.json');
70+
if (await this.fs.pathExists(possiblePackageJson)) {
71+
const text = await this.fs.readFile(possiblePackageJson);
72+
try {
73+
const json = JSON.parse(text);
74+
return { extensionId: `${json.publisher}.${json.name}`, displayName: json.displayName };
75+
} catch {
76+
// If parse fails, then not an extension.
77+
}
78+
}
79+
last = dirName;
80+
dirName = path.dirname(dirName);
81+
}
82+
}
83+
}
84+
return { extensionId: 'unknown', displayName: 'unknown' };
85+
}
2386
}

extensions/positron-python/src/client/common/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,11 @@ export interface IExtensions {
428428
* @return An extension or `undefined`.
429429
*/
430430
getExtension<T>(extensionId: string): Extension<T> | undefined;
431+
432+
/**
433+
* Determines which extension called into our extension code based on call stacks.
434+
*/
435+
determineExtensionFromCallStack(): Promise<{ extensionId: string; displayName: string }>;
431436
}
432437

433438
export const IBrowserService = Symbol('IBrowserService');

0 commit comments

Comments
 (0)