Skip to content

Commit

Permalink
Finalizing design of proposed API for python environments (microsoft#…
Browse files Browse the repository at this point in the history
…19841)

Closes microsoft#19101 closes
microsoft#18973

Co-authored-by: Karthik Nadig <[email protected]>
  • Loading branch information
2 people authored and eleanorjboyd committed Oct 4, 2022
1 parent ae9e272 commit aeb80cf
Show file tree
Hide file tree
Showing 21 changed files with 1,309 additions and 592 deletions.
1 change: 0 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,6 @@ src/client/common/application/languageService.ts
src/client/common/application/clipboard.ts
src/client/common/application/workspace.ts
src/client/common/application/debugSessionTelemetry.ts
src/client/common/application/extensions.ts
src/client/common/application/documentManager.ts
src/client/common/application/debugService.ts
src/client/common/application/commands/reloadCommand.ts
Expand Down
18 changes: 15 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -1816,6 +1816,7 @@
"request-progress": "^3.0.0",
"rxjs": "^6.5.4",
"rxjs-compat": "^6.5.4",
"stack-trace": "0.0.10",
"semver": "^5.5.0",
"sudo-prompt": "^9.2.1",
"tmp": "^0.0.33",
Expand Down Expand Up @@ -1852,11 +1853,12 @@
"@types/semver": "^5.5.0",
"@types/shortid": "^0.0.29",
"@types/sinon": "^10.0.11",
"@types/stack-trace": "0.0.29",
"@types/tmp": "^0.0.33",
"@types/uuid": "^8.3.4",
"@types/vscode": "~1.68.0",
"@types/winreg": "^1.2.30",
"@types/which": "^2.0.1",
"@types/winreg": "^1.2.30",
"@types/xml2js": "^0.4.2",
"@typescript-eslint/eslint-plugin": "^3.7.0",
"@typescript-eslint/parser": "^3.7.0",
Expand Down
136 changes: 0 additions & 136 deletions src/client/apiTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
import { Event, Uri } from 'vscode';
import { Resource } from './common/types';
import { IDataViewerDataProvider, IJupyterUriProvider } from './jupyter/types';
import { EnvPathType, PythonEnvKind } from './pythonEnvironments/base/info';
import { GetRefreshEnvironmentsOptions, ProgressNotificationEvent } from './pythonEnvironments/base/locator';

/*
* Do not introduce any breaking changes to this API.
Expand Down Expand Up @@ -88,137 +86,3 @@ export interface IExtensionApi {
registerRemoteServerProvider(serverProvider: IJupyterUriProvider): void;
};
}

export interface EnvironmentDetailsOptions {
useCache: boolean;
}

export interface EnvironmentDetails {
interpreterPath: string;
envFolderPath?: string;
version: string[];
environmentType: PythonEnvKind[];
metadata: Record<string, unknown>;
}

export interface EnvironmentsChangedParams {
/**
* Path to environment folder or path to interpreter that uniquely identifies an environment.
* Virtual environments lacking an interpreter are identified by environment folder paths,
* whereas other envs can be identified using interpreter path.
*/
path?: string;
type: 'add' | 'remove' | 'update' | 'clear-all';
}

export interface ActiveEnvironmentChangedParams {
/**
* Path to environment folder or path to interpreter that uniquely identifies an environment.
* Virtual environments lacking an interpreter are identified by environment folder paths,
* whereas other envs can be identified using interpreter path.
*/
path: string;
resource?: Uri;
}

export interface IProposedExtensionAPI {
environment: {
/**
* An event that is emitted when execution details (for a resource) change. For instance, when interpreter configuration changes.
*/
readonly onDidChangeExecutionDetails: Event<Uri | undefined>;
/**
* Returns all the details the consumer needs to execute code within the selected environment,
* corresponding to the specified resource taking into account any workspace-specific settings
* for the workspace to which this resource belongs.
* @param {Resource} [resource] A resource for which the setting is asked for.
* * When no resource is provided, the setting scoped to the first workspace folder is returned.
* * If no folder is present, it returns the global setting.
* @returns {({ execCommand: string[] | undefined })}
*/
getExecutionDetails(
resource?: Resource,
): Promise<{
/**
* E.g of execution commands returned could be,
* * `['<path to the interpreter set in settings>']`
* * `['<path to the interpreter selected by the extension when setting is not set>']`
* * `['conda', 'run', 'python']` which is used to run from within Conda environments.
* or something similar for some other Python environments.
*
* @type {(string[] | undefined)} When return value is `undefined`, it means no interpreter is set.
* Otherwise, join the items returned using space to construct the full execution command.
*/
execCommand: string[] | undefined;
}>;
/**
* Returns the path to the python binary selected by the user or as in the settings.
* This is just the path to the python binary, this does not provide activation or any
* other activation command. The `resource` if provided will be used to determine the
* python binary in a multi-root scenario. If resource is `undefined` then the API
* returns what ever is set for the workspace.
* @param resource : Uri of a file or workspace
*/
getActiveEnvironmentPath(resource?: Resource): Promise<EnvPathType | undefined>;
/**
* Returns details for the given interpreter. Details such as absolute interpreter path,
* version, type (conda, pyenv, etc). Metadata such as `sysPrefix` can be found under
* metadata field.
* @param path : Full path to environment folder or interpreter whose details you need.
* @param options : [optional]
* * useCache : When true, cache is checked first for any data, returns even if there
* is partial data.
*/
getEnvironmentDetails(
path: string,
options?: EnvironmentDetailsOptions,
): Promise<EnvironmentDetails | undefined>;
/**
* Returns paths to environments that uniquely identifies an environment found by the extension
* at the time of calling. This API will *not* trigger a refresh. If a refresh is going on it
* will *not* wait for the refresh to finish. This will return what is known so far. To get
* complete list `await` on promise returned by `getRefreshPromise()`.
*
* Virtual environments lacking an interpreter are identified by environment folder paths,
* whereas other envs can be identified using interpreter path.
*/
getEnvironmentPaths(): Promise<EnvPathType[] | undefined>;
/**
* Sets the active environment path for the python extension for the resource. Configuration target
* will always be the workspace folder.
* @param path : Full path to environment folder or interpreter to set.
* @param resource : [optional] Uri of a file ro workspace to scope to a particular workspace
* folder.
*/
setActiveEnvironment(path: string, resource?: Resource): Promise<void>;
/**
* This API will re-trigger environment discovery. Extensions can wait on the returned
* promise to get the updated environment list. If there is a refresh already going on
* then it returns the promise for that refresh.
* @param options : [optional]
* * clearCache : When true, this will clear the cache before environment refresh
* is triggered.
*/
refreshEnvironment(): Promise<EnvPathType[] | undefined>;
/**
* Tracks discovery progress for current list of known environments, i.e when it starts, finishes or any other relevant
* stage. Note the progress for a particular query is currently not tracked or reported, this only indicates progress of
* the entire collection.
*/
readonly onRefreshProgress: Event<ProgressNotificationEvent>;
/**
* Returns a promise for the ongoing refresh. Returns `undefined` if there are no active
* refreshes going on.
*/
getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise<void> | undefined;
/**
* This event is triggered when the known environment list changes, like when a environment
* is found, existing environment is removed, or some details changed on an environment.
*/
onDidEnvironmentsChanged: Event<EnvironmentsChangedParams[]>;
/**
* This event is triggered when the active environment changes.
*/
onDidActiveEnvironmentChanged: Event<ActiveEnvironmentChangedParams>;
};
}
69 changes: 66 additions & 3 deletions src/client/common/application/extensions.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
/* eslint-disable class-methods-use-this */
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

'use strict';

import { injectable } from 'inversify';
import { inject, injectable } from 'inversify';
import { Event, Extension, extensions } from 'vscode';
import * as stacktrace from 'stack-trace';
import * as path from 'path';
import { IExtensions } from '../types';
import { IFileSystem } from '../platform/types';
import { EXTENSION_ROOT_DIR } from '../constants';

/**
* Provides functions for tracking the list of extensions that VSCode has installed.
*/
@injectable()
export class Extensions implements IExtensions {
constructor(@inject(IFileSystem) private readonly fs: IFileSystem) {}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
public get all(): readonly Extension<any>[] {
return extensions.all;
}
Expand All @@ -17,7 +28,59 @@ export class Extensions implements IExtensions {
return extensions.onDidChange;
}

public getExtension(extensionId: any) {
public getExtension(extensionId: string): Extension<unknown> | undefined {
return extensions.getExtension(extensionId);
}

/**
* Code borrowed from:
* https://github.com/microsoft/vscode-jupyter/blob/67fe33d072f11d6443cf232a06bed0ac5e24682c/src/platform/common/application/extensions.node.ts
*/
public async determineExtensionFromCallStack(): Promise<{ extensionId: string; displayName: string }> {
const { stack } = new Error();
if (stack) {
const pythonExtRoot = path.join(EXTENSION_ROOT_DIR.toLowerCase(), path.sep);
const frames = stack
.split('\n')
.map((f) => {
const result = /\((.*)\)/.exec(f);
if (result) {
return result[1];
}
return undefined;
})
.filter((item) => item && !item.toLowerCase().startsWith(pythonExtRoot))
.filter((item) =>
this.all.some(
(ext) => item!.includes(ext.extensionUri.path) || item!.includes(ext.extensionUri.fsPath),
),
) as string[];
stacktrace.parse(new Error('Ex')).forEach((item) => {
const fileName = item.getFileName();
if (fileName && !fileName.toLowerCase().startsWith(pythonExtRoot)) {
frames.push(fileName);
}
});
for (const frame of frames) {
// This file is from a different extension. Try to find its `package.json`.
let dirName = path.dirname(frame);
let last = frame;
while (dirName && dirName.length < last.length) {
const possiblePackageJson = path.join(dirName, 'package.json');
if (await this.fs.pathExists(possiblePackageJson)) {
const text = await this.fs.readFile(possiblePackageJson);
try {
const json = JSON.parse(text);
return { extensionId: `${json.publisher}.${json.name}`, displayName: json.displayName };
} catch {
// If parse fails, then not an extension.
}
}
last = dirName;
dirName = path.dirname(dirName);
}
}
}
return { extensionId: 'unknown', displayName: 'unknown' };
}
}
5 changes: 5 additions & 0 deletions src/client/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,11 @@ export interface IExtensions {
* @return An extension or `undefined`.
*/
getExtension<T>(extensionId: string): Extension<T> | undefined;

/**
* Determines which extension called into our extension code based on call stacks.
*/
determineExtensionFromCallStack(): Promise<{ extensionId: string; displayName: string }>;
}

export const IBrowserService = Symbol('IBrowserService');
Expand Down
Loading

0 comments on commit aeb80cf

Please sign in to comment.