diff --git a/src/client/apiTypes.ts b/src/client/apiTypes.ts index 6361a75edb48..a6ab62fcb972 100644 --- a/src/client/apiTypes.ts +++ b/src/client/apiTypes.ts @@ -1,11 +1,11 @@ +/* eslint-disable @typescript-eslint/ban-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. 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. @@ -88,137 +88,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; -} - -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; - /** - * 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, - * * `['']` - * * `['']` - * * `['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; - /** - * 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; - /** - * 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; - /** - * 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; - /** - * 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; - /** - * 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; - /** - * Returns a promise for the ongoing refresh. Returns `undefined` if there are no active - * refreshes going on. - */ - getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise | 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; - /** - * This event is triggered when the active environment changes. - */ - onDidActiveEnvironmentChanged: Event; - }; -} diff --git a/src/client/extension.ts b/src/client/extension.ts index 312e99a38683..30da91a0d2ba 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -42,10 +42,11 @@ import { sendErrorTelemetry, sendStartupTelemetry } from './startupTelemetry'; import { IStartupDurations } from './types'; import { runAfterActivation } from './common/utils/runAfterActivation'; import { IInterpreterService } from './interpreter/contracts'; -import { IExtensionApi, IProposedExtensionAPI } from './apiTypes'; +import { IExtensionApi } from './apiTypes'; import { buildProposedApi } from './proposedApi'; import { WorkspaceService } from './common/application/workspace'; import { disposeAll } from './common/utils/resourceLifecycle'; +import { IProposedExtensionAPI } from './proposedApiTypes'; durations.codeLoadingTime = stopWatch.elapsedTime; diff --git a/src/client/interpreter/interpreterService.ts b/src/client/interpreter/interpreterService.ts index cc4bf786dd6d..270280e500e2 100644 --- a/src/client/interpreter/interpreterService.ts +++ b/src/client/interpreter/interpreterService.ts @@ -11,7 +11,7 @@ import { Uri, } from 'vscode'; import '../common/extensions'; -import { IApplicationShell, IDocumentManager } from '../common/application/types'; +import { IApplicationShell, IDocumentManager, IWorkspaceService } from '../common/application/types'; import { IConfigurationService, IDisposableRegistry, @@ -221,8 +221,8 @@ export class InterpreterService implements Disposable, IInterpreterService { this._pythonPathSetting = pySettings.pythonPath; this.didChangeInterpreterEmitter.fire(); reportActiveInterpreterChanged({ - path: pySettings.pythonPath, - resource, + pathID: pySettings.pythonPath, + resource: this.serviceContainer.get(IWorkspaceService).getWorkspaceFolder(resource), }); const interpreterDisplay = this.serviceContainer.get(IInterpreterDisplay); interpreterDisplay.refresh().catch((ex) => traceError('Python Extension: display.refresh', ex)); diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index fc432efeb821..d02a0f670fbc 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -2,17 +2,17 @@ // Licensed under the MIT License. import { ConfigurationTarget, EventEmitter } from 'vscode'; -import { - ActiveEnvironmentChangedParams, - EnvironmentDetails, - EnvironmentDetailsOptions, - EnvironmentsChangedParams, - IProposedExtensionAPI, -} from './apiTypes'; import { arePathsSame } from './common/platform/fs-paths'; import { IInterpreterPathService, Resource } from './common/types'; import { IInterpreterService } from './interpreter/contracts'; import { IServiceContainer } from './ioc/types'; +import { + EnvironmentsChangedParams, + ActiveEnvironmentChangedParams, + IProposedExtensionAPI, + EnvironmentDetailsOptions, + EnvironmentDetails, +} from './proposedApiTypes'; import { PythonEnvInfo } from './pythonEnvironments/base/info'; import { getEnvPath } from './pythonEnvironments/base/info/env'; import { GetRefreshEnvironmentsOptions, IDiscoveryAPI } from './pythonEnvironments/base/locator'; @@ -32,7 +32,7 @@ function getVersionString(env: PythonEnvInfo): string[] { if (env.version.release) { ver.push(`${env.version.release}`); if (env.version.sysVersion) { - ver.push(`${env.version.release}`); + ver.push(`${env.version.sysVersion}`); } } return ver; diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts new file mode 100644 index 000000000000..3b3c9c125106 --- /dev/null +++ b/src/client/proposedApiTypes.ts @@ -0,0 +1,249 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Event, Uri, WorkspaceFolder } from 'vscode'; + +// https://github.com/microsoft/vscode-python/wiki/Proposed-Environment-APIs + +export interface IProposedExtensionAPI { + environment: { + /** + * This event is triggered when the active environment changes. + */ + onDidChangeActiveEnvironment: Event; + /** + * 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; + /** + * Returns details for the given python executable. Details such as absolute python executable path, + * version, type (conda, pyenv, etc). Metadata such as `sysPrefix` can be found under + * metadata field. + * @param pathID : Full path to environment folder or python executable 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( + pathID: UniquePathType | EnvironmentPath, + options?: EnvironmentDetailsOptions, + ): Promise; + /** + * Sets the active environment path for the python extension for the resource. Configuration target + * will always be the workspace folder. + * @param pathID : Full path to environment folder or python executable to set. + * @param resource : [optional] Uri of a file ro workspace to scope to a particular workspace + * folder. + */ + setActiveEnvironment(pathID: UniquePathType | EnvironmentPath, resource?: Resource): Promise; + locator: { + /** + * Returns paths to environments that uniquely identifies an environment found by the extension + * at the time of calling. It returns the values currently in memory. 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()`. + */ + getEnvironmentPaths(): EnvironmentPath[] | 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. + */ + onDidChangeEnvironments: Event; + /** + * 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. + */ + refreshEnvironments(options?: RefreshEnvironmentsOptions): Promise; + /** + * Returns a promise for the ongoing refresh. Returns `undefined` if there are no active + * refreshes going on. + */ + getRefreshPromise(options?: GetRefreshPromiseOptions): Promise | 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; + }; + }; +} + +export enum Architecture { + Unknown = 1, + x86 = 2, + x64 = 3, +} + +export type EnvSource = KnownEnvSources | string; +export type KnownEnvSources = 'Conda' | 'Pipenv' | 'Poetry' | 'VirtualEnv' | 'Venv' | 'VirtualEnvWrapper' | 'Pyenv'; + +export type EnvType = KnownEnvTypes | string; +export type KnownEnvTypes = 'VirtualEnv' | 'Conda' | 'Unknown'; + +export type BasicVersionInfo = { + major: number; + minor: number; + micro: number; +}; + +/** + * The possible Python release levels. + */ +export enum PythonReleaseLevel { + Alpha = 'alpha', + Beta = 'beta', + Candidate = 'candidate', + Final = 'final', +} + +/** + * Release information for a Python version. + */ +export type PythonVersionRelease = { + level: PythonReleaseLevel; + serial: number; +}; + +export type StandardVersionInfo = BasicVersionInfo & { + release?: PythonVersionRelease; +}; + +// To be added later: +// run: { +// exec: Function; +// shellExec: Function; +// execObservable: Function; +// terminalExec: () => void; +// env?: { [key: string]: string | null | undefined }; // Should be specific to the current terminal?? +// }; + +export interface EnvironmentDetails { + executable: { + path: string; + bitness?: Architecture; + sysPrefix: string; + }; + environment: + | { + type: EnvType; + name?: string; + folderPath: string; + /** + * Any specific workspace folder this environment is created for. + * What if that workspace folder is not opened yet? We should still provide a workspace folder so it can be filtered out. + * WorkspaceFolder type won't work as it assumes the workspace is opened, hence using URI. + */ + workspaceFolder?: Uri; + source: EnvSource[]; + } + | undefined; + version: StandardVersionInfo & { + sysVersion?: string; + }; + implementation?: { + // `sys.implementation` + name: string; + version: StandardVersionInfo; + }; +} + +export interface EnvironmentDetailsOptions { + /** + * When true, cache is checked first for any data, returns even if there is partial data. + */ + useCache: boolean; +} + +export interface GetRefreshPromiseOptions { + /** + * Get refresh promise which resolves once the following stage has been reached for the list of known environments. + */ + stage?: ProgressReportStage; +} + +export enum ProgressReportStage { + discoveryStarted = 'discoveryStarted', + allPathsDiscovered = 'allPathsDiscovered', + discoveryFinished = 'discoveryFinished', +} + +export type ProgressNotificationEvent = { + stage: ProgressReportStage; +}; + +/** + * Uri of a file inside a workspace or workspace folder itself. + */ +export type Resource = Uri | WorkspaceFolder; + +/** + * Path to environment folder or path to python executable that uniquely identifies an environment. + * Environments lacking a python executable are identified by environment folder paths, + * whereas other envs can be identified using python executable path. + */ +export type UniquePathType = string; + +export interface EnvironmentPath { + pathID: UniquePathType; + /** + * Path to python executable that uniquely identifies an environment. + * Carries `undefined` if an executable cannot uniquely identify an + * environment or does not exist within the env. + */ + executablePath: string | undefined; +} + +export type EnvironmentsChangedParams = + | ({ + /** + * * "add": New environment is added. + * * "remove": Existing environment in the list is removed. + * * "update": New information found about existing environment. + */ + type: 'add' | 'remove' | 'update'; + } & EnvironmentPath) + | { + /** + * * "clear-all": Remove all of the items in the list. (This is fired when a hard refresh is triggered) + */ + type: 'clear-all'; + } + | { + /** + * The location at which the environment got created. + */ + location: string; + /** + * * "created": New environment is created in some location. + */ + type: 'created'; + }; + +export interface ActiveEnvironmentChangedParams { + pathID: UniquePathType; + /** + * Workspace folder the environment changed for. + */ + resource: WorkspaceFolder | undefined; +} + +export interface RefreshEnvironmentsOptions { + /** + * When `true`, this will clear the cache before environment refresh is triggered. + */ + clearCache?: boolean; + /** + * Only trigger a refresh if it hasn't already been triggered for this session. + */ + ifNotTriggerredAlready?: boolean; +} diff --git a/src/client/pythonEnvironments/api.ts b/src/client/pythonEnvironments/api.ts index b9c3152a0b67..2a0fc5ea5f96 100644 --- a/src/client/pythonEnvironments/api.ts +++ b/src/client/pythonEnvironments/api.ts @@ -20,6 +20,8 @@ export type GetLocatorFunc = () => Promise; class PythonEnvironments implements IDiscoveryAPI { private locator!: IDiscoveryAPI; + public addNewProvider = this.locator.addNewProvider; + constructor( // These are factories for the sub-components the full component is composed of: private readonly getLocator: GetLocatorFunc, @@ -27,6 +29,7 @@ class PythonEnvironments implements IDiscoveryAPI { public async activate(): Promise { this.locator = await this.getLocator(); + this.addNewProvider = this.locator.addNewProvider; } public get onProgress(): Event { diff --git a/src/client/pythonEnvironments/base/info/env.ts b/src/client/pythonEnvironments/base/info/env.ts index e65339b78c27..efa2a6593697 100644 --- a/src/client/pythonEnvironments/base/info/env.ts +++ b/src/client/pythonEnvironments/base/info/env.ts @@ -20,7 +20,7 @@ import { PythonVersion, virtualEnvKinds, } from '.'; -import { BasicEnvInfo } from '../locator'; +import { CompositeEnvInfo, convertKindIntoArray } from '../locator'; /** * Create a new info object with all values empty. @@ -28,7 +28,7 @@ import { BasicEnvInfo } from '../locator'; * @param init - if provided, these values are applied to the new object */ export function buildEnvInfo(init?: { - kind?: PythonEnvKind; + kind?: PythonEnvKind[] | PythonEnvKind; executable?: string; name?: string; location?: string; @@ -44,7 +44,7 @@ export function buildEnvInfo(init?: { const env: PythonEnvInfo = { name: init?.name ?? '', location: '', - kind: PythonEnvKind.Unknown, + kind: [PythonEnvKind.Unknown], executable: { filename: '', sysPrefix: init?.sysPrefix ?? '', @@ -83,7 +83,7 @@ export function buildEnvInfo(init?: { export function copyEnvInfo( env: PythonEnvInfo, updates?: { - kind?: PythonEnvKind; + kind?: PythonEnvKind[] | PythonEnvKind; }, ): PythonEnvInfo { // We don't care whether or not extra/hidden properties @@ -98,7 +98,7 @@ export function copyEnvInfo( function updateEnv( env: PythonEnvInfo, updates: { - kind?: PythonEnvKind; + kind?: PythonEnvKind[] | PythonEnvKind; executable?: string; location?: string; version?: PythonVersion; @@ -106,7 +106,7 @@ function updateEnv( }, ): void { if (updates.kind !== undefined) { - env.kind = updates.kind; + env.kind = convertKindIntoArray(updates.kind); } if (updates.executable !== undefined) { env.executable.filename = updates.executable; @@ -135,8 +135,8 @@ export function setEnvDisplayString(env: PythonEnvInfo): void { function buildEnvDisplayString(env: PythonEnvInfo, getAllDetails = false): string { // main parts - const shouldDisplayKind = getAllDetails || env.searchLocation || globallyInstalledEnvKinds.includes(env.kind); - const shouldDisplayArch = !virtualEnvKinds.includes(env.kind); + const shouldDisplayKind = getAllDetails || env.searchLocation || globallyInstalledEnvKinds.includes(env.kind[0]); + const shouldDisplayArch = !virtualEnvKinds.includes(env.kind[0]); const displayNameParts: string[] = ['Python']; if (env.version && !isVersionEmpty(env.version)) { displayNameParts.push(getVersionDisplayString(env.version)); @@ -156,7 +156,7 @@ function buildEnvDisplayString(env: PythonEnvInfo, getAllDetails = false): strin envSuffixParts.push(`'${env.name}'`); } if (shouldDisplayKind) { - const kindName = getKindDisplayName(env.kind); + const kindName = getKindDisplayName(env.kind[0]); if (kindName !== '') { envSuffixParts.push(kindName); } @@ -173,7 +173,7 @@ function buildEnvDisplayString(env: PythonEnvInfo, getAllDetails = false): strin * If insufficient data is provided to generate a minimal object, such * that it is not identifiable, then `undefined` is returned. */ -function getMinimalPartialInfo(env: string | PythonEnvInfo | BasicEnvInfo): Partial | undefined { +function getMinimalPartialInfo(env: string | PythonEnvInfo | CompositeEnvInfo): Partial | undefined { if (typeof env === 'string') { if (env === '') { return undefined; @@ -235,8 +235,8 @@ export function getEnvID(interpreterPath: string, envFolderPath?: string): strin * where multiple versions of python executables are all put in the same directory. */ export function areSameEnv( - left: string | PythonEnvInfo | BasicEnvInfo, - right: string | PythonEnvInfo | BasicEnvInfo, + left: string | PythonEnvInfo | CompositeEnvInfo, + right: string | PythonEnvInfo | CompositeEnvInfo, allowPartialMatch = true, ): boolean | undefined { const leftInfo = getMinimalPartialInfo(left); diff --git a/src/client/pythonEnvironments/base/info/envKind.ts b/src/client/pythonEnvironments/base/info/envKind.ts index 09490725e960..165e2a572385 100644 --- a/src/client/pythonEnvironments/base/info/envKind.ts +++ b/src/client/pythonEnvironments/base/info/envKind.ts @@ -70,3 +70,27 @@ export function getPrioritizedEnvKinds(): PythonEnvKind[] { PythonEnvKind.Unknown, ]; } + +const envKindByPriority = getPrioritizedEnvKinds(); + +export const sortKindFunction = (a: PythonEnvKind, b: PythonEnvKind): number => + envKindByPriority.indexOf(a) - envKindByPriority.indexOf(b); + +/** + * Sorts which extension id should be preferred for resolving, identification, reducing etc. + * + * Note `extensionId` property is considered `undefined` if env is discovered by Python extension. + */ +export const sortExtensionSource = (extensionId1: string | undefined, extensionId2: string | undefined): number => { + // If another extension provides an env, prefer that over what Python extension provides. + if (extensionId1) { + if (extensionId2) { + return sortExtensionSource(extensionId1, extensionId2); + } + return -1; + } + if (extensionId2) { + return 1; + } + return 0; +}; diff --git a/src/client/pythonEnvironments/base/info/environmentInfoService.ts b/src/client/pythonEnvironments/base/info/environmentInfoService.ts index c6d4308bee89..75a92fac6c57 100644 --- a/src/client/pythonEnvironments/base/info/environmentInfoService.ts +++ b/src/client/pythonEnvironments/base/info/environmentInfoService.ts @@ -102,7 +102,7 @@ class EnvironmentInfoService implements IEnvironmentInfoService { env: PythonEnvInfo, priority?: EnvironmentInfoServiceQueuePriority, ): Promise { - if (env.kind === PythonEnvKind.Conda && env.executable.filename === 'python') { + if (env.kind.includes(PythonEnvKind.Conda) && env.executable.filename === 'python') { const emptyInterpreterInfo: InterpreterInformation = { arch: Architecture.Unknown, executable: { @@ -131,7 +131,8 @@ class EnvironmentInfoService implements IEnvironmentInfoService { if (r === undefined) { // Even though env kind is not conda, it can still be a conda environment // as complete env info may not be available at this time. - const isCondaEnv = env.kind === PythonEnvKind.Conda || (await isCondaEnvironment(env.executable.filename)); + const isCondaEnv = + env.kind.includes(PythonEnvKind.Conda) || (await isCondaEnvironment(env.executable.filename)); if (isCondaEnv) { traceInfo( `Validating ${env.executable.filename} normally failed with error, falling back to using conda run: (${reason})`, diff --git a/src/client/pythonEnvironments/base/info/index.ts b/src/client/pythonEnvironments/base/info/index.ts index 13d0b29a96e8..33b36a1bdeb6 100644 --- a/src/client/pythonEnvironments/base/info/index.ts +++ b/src/client/pythonEnvironments/base/info/index.ts @@ -26,13 +26,15 @@ export enum PythonEnvKind { OtherVirtual = 'virt-other', } +export type UniquePathType = string; + export interface EnvPathType { /** * Path to environment folder or path to interpreter that uniquely identifies an environment. - * Virtual environments lacking an interpreter are identified by environment folder paths, + * Environments lacking an interpreter are identified by environment folder paths, * whereas other envs can be identified using interpreter path. */ - path: string; + path: UniquePathType; pathType: 'envFolderPath' | 'interpreterPath'; } @@ -104,7 +106,7 @@ export enum PythonEnvSource { */ type PythonEnvBaseInfo = { id?: string; - kind: PythonEnvKind; + kind: PythonEnvKind[]; executable: PythonExecutableInfo; // One of (name, location) must be non-empty. name: string; @@ -135,13 +137,16 @@ export type PythonVersionRelease = { serial: number; }; +export type StandardVersionInfo = BasicVersionInfo & { + release?: PythonVersionRelease; +}; + /** * Version information for a Python build/installation. * * @prop sysVersion - the raw text from `sys.version` */ -export type PythonVersion = BasicVersionInfo & { - release?: PythonVersionRelease; +export type PythonVersion = StandardVersionInfo & { sysVersion?: string; }; diff --git a/src/client/pythonEnvironments/base/locator.ts b/src/client/pythonEnvironments/base/locator.ts index c0d1cd23991c..bb541da72230 100644 --- a/src/client/pythonEnvironments/base/locator.ts +++ b/src/client/pythonEnvironments/base/locator.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/ban-types */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. @@ -5,7 +6,8 @@ import { Event, Uri } from 'vscode'; import { IAsyncIterableIterator, iterEmpty } from '../../common/utils/async'; -import { PythonEnvInfo, PythonEnvKind, PythonEnvSource } from './info'; +import { Architecture } from '../../common/utils/platform'; +import { PythonEnvInfo, PythonEnvKind, PythonEnvSource, StandardVersionInfo, UniquePathType } from './info'; import { BasicPythonEnvsChangedEvent, IPythonEnvsWatcher, @@ -14,6 +16,172 @@ import { PythonEnvsWatcher, } from './watcher'; +export interface EnvironmentDetailsOptions { + useCache: boolean; +} + +export interface EnvironmentDetails { + executable: { + path: string; + bitness?: Architecture; + sysPrefix: string; + }; + environment: + | { + type: EnvType; + name?: string; + path: string; + project?: string; // Any specific project environment is created for. + source: EnvSource[]; + } + | undefined; + version: StandardVersionInfo & { + sysVersion?: string; + }; + implementation?: { + // `sys.implementation` + name: string; + version: StandardVersionInfo; + }; +} + +/** + * Provider is only required to provide the `executable` key, rest are optional. So construct a type using + * `EnvironmentDetails` where `executable` is the only required key. + */ +type EnvironmentDetailsByProvider = Partial & + Pick & + Pick; + +export type IInternalEnvironmentProvider = ILocatorFactoryAPI & IInternalResolverAPI; + +interface ILocatorFactoryAPI { + /** + * Factory function calling which create the locator. + */ + createLocator: ILocatorFactory; +} + +export type ProposedDetailsAPI = (env: BaseEnvInfo) => Promise; +export type InternalDetailsAPI = (env: BasicEnvInfo) => Promise; +export interface IResolverAPI { + /** + * Environment source the provider identifies/resolves. + */ + readonly envSource: EnvSource | undefined; + /** + * Returns true if provided environment comes from the specified env source. + */ + canIdentifyEnvironment: (path: UniquePathType) => Promise; + /** + * Returns details or `undefined` if it was found if env is invalid. + * This is only called if: + * * The provider can identify the environment. + * * To get more details out of an environment already iterated by the provider. + */ + getEnvironmentDetails: ProposedDetailsAPI; +} + +export interface IInternalResolverAPI { + readonly envKind: PythonEnvKind | undefined; + canIdentifyEnvironment: (path: UniquePathType) => Promise; + getEnvironmentDetails: InternalDetailsAPI; +} + +export type ILocatorFactory = IWorkspaceLocatorFactory | INonWorkspaceLocatorFactory; +export type INonWorkspaceLocatorFactory = () => ILocatorAPI; +export type IWorkspaceLocatorFactory = (root: string) => ILocatorAPI; + +export type IEnvironmentProvider = ILocatorFactoryAPI & IResolverAPI; +export interface ILocatorAPI { + iterEnvs(): IPythonEnvsIterator; + readonly onChanged: Event; +} + +export type EnvInfo = BaseEnvInfo & { + envSource: EnvSource[] | EnvSource; +}; + +export type BaseEnvInfo = { + executablePath: string; + envPath?: string; +}; + +type ExtensionID = string; + +/** + * These can be used when querying for a particular env. + */ +export interface EnvironmentProviderMetadata { + /** + * Details about the environments the locator provides. + * Useful when querying for a particular env. + */ + readonly environments?: EnvironmentMetaData; + /** + * An Identifier for the extension registering the provider. + */ + readonly extensionId: ExtensionID; +} + +interface InternalEnvironmentMetaData { + readonly envKinds: PythonEnvKind[]; +} + +/** + * These can be used when querying for a particular env. + */ +export interface InternalEnvironmentProviderMetadata { + /** + * Details about the environments the locator provides. + * Useful when querying for a particular env. + */ + readonly environments: InternalEnvironmentMetaData; + readonly extensionId: ExtensionID; +} + +interface EnvironmentMetaData { + readonly envType: EnvType; + readonly envSources?: EnvSource[]; +} + +export interface LocatorEnvsChangedEvent { + /** + * Details about how the environment was modified. + * */ + type: EnvChangeType; + /** + * The unique ID for the environment affected. + */ + pathId: UniquePathType; + /** + * Any other details known about the environment which can be used for query. + */ + env?: EnvironmentMetaData; +} + +export type EnvChangeType = 'add' | 'remove' | 'update'; + +export type EnvType = KnownEnvTypes | string; + +export enum KnownEnvTypes { + VirtualEnv = 'VirtualEnv', + Conda = 'Conda', + Unknown = 'Unknown', +} + +export type EnvSource = KnownEnvSourceTypes | string; + +export enum KnownEnvSourceTypes { + Conda = 'Conda', + Pipenv = 'PipEnv', + Poetry = 'Poetry', + VirtualEnv = 'VirtualEnv', + Venv = 'Venv', + VirtualEnvWrapper = 'VirtualEnvWrapper', + Pyenv = 'Pyenv', +} + /** * A single update to a previously provided Python env object. */ @@ -132,13 +300,31 @@ export type PythonLocatorQuery = BasicPythonLocatorQuery & { type QueryForEvent = E extends PythonEnvsChangedEvent ? PythonLocatorQuery : BasicPythonLocatorQuery; -export type BasicEnvInfo = { - kind: PythonEnvKind; +export type BasicEnvInfo = { + kind: T; executablePath: string; source?: PythonEnvSource[]; envPath?: string; + extensionId?: ExtensionID; }; +/** + * A version of `BasicEnvInfo` used for composite locators. + */ +export type CompositeEnvInfo = BasicEnvInfo; + +export function convertBasicToComposite(env: BasicEnvInfo): CompositeEnvInfo { + env.kind = convertKindIntoArray(env.kind); + return env as CompositeEnvInfo; +} + +export function convertKindIntoArray(kind: PythonEnvKind | PythonEnvKind[]): PythonEnvKind[] { + if (!Array.isArray(kind)) { + kind = [kind]; + } + return kind; +} + /** * A single Python environment locator. * @@ -154,7 +340,8 @@ export type BasicEnvInfo = { * for the specific environments that changed. */ export interface ILocator - extends IPythonEnvsWatcher { + extends IPythonEnvsWatcher, + ILocatorRegister { /** * Iterate over the enviroments known tos this locator. * @@ -174,6 +361,17 @@ export interface ILocator): IPythonEnvsIterator; } +export interface ILocatorRegister { + addNewLocator?(locatorFactory: ILocatorFactory, metadata: InternalEnvironmentProviderMetadata): void; +} + +export interface IEnvProviderRegister { + addNewProvider?( + environmentProvider: IInternalEnvironmentProvider, + metadata: InternalEnvironmentProviderMetadata, + ): void; +} + interface IResolver { /** * Find as much info about the given Python environment as possible. @@ -184,7 +382,7 @@ interface IResolver { resolveEnv(path: string): Promise; } -export interface IResolvingLocator extends IResolver, ILocator {} +export interface IResolvingLocator extends IResolver, ILocator, IEnvProviderRegister {} export interface GetRefreshEnvironmentsOptions { /** @@ -200,7 +398,7 @@ export type TriggerRefreshOptions = { ifNotTriggerredAlready?: boolean; }; -export interface IDiscoveryAPI { +export interface IDiscoveryAPI extends IEnvProviderRegister { /** * 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 diff --git a/src/client/pythonEnvironments/base/locatorUtils.ts b/src/client/pythonEnvironments/base/locatorUtils.ts index 97cb6298416f..ebb01b798552 100644 --- a/src/client/pythonEnvironments/base/locatorUtils.ts +++ b/src/client/pythonEnvironments/base/locatorUtils.ts @@ -26,7 +26,7 @@ export function getQueryFilter(query: PythonLocatorQuery): (env: PythonEnvInfo) if (kinds === undefined) { return true; } - return kinds.includes(env.kind); + return kinds.includes(env.kind[0]); } function checkSearchLocation(env: PythonEnvInfo): boolean { if (env.searchLocation === undefined) { diff --git a/src/client/pythonEnvironments/base/locators.ts b/src/client/pythonEnvironments/base/locators.ts index 4c854e975ecf..886f4f6e1258 100644 --- a/src/client/pythonEnvironments/base/locators.ts +++ b/src/client/pythonEnvironments/base/locators.ts @@ -3,8 +3,8 @@ import { chain } from '../../common/utils/async'; import { Disposables } from '../../common/utils/resourceLifecycle'; -import { PythonEnvInfo } from './info'; import { + BasicEnvInfo, ILocator, IPythonEnvsIterator, isProgressEvent, @@ -59,10 +59,10 @@ export function combineIterators(iterators: IPythonEnvsIterator[]): IPytho * * Events and iterator results are combined. */ -export class Locators extends PythonEnvsWatchers implements ILocator { +export class Locators extends PythonEnvsWatchers implements ILocator { constructor( // The locators will be watched as well as iterated. - private readonly locators: ReadonlyArray>, + private locators: ILocator[], ) { super(locators); } @@ -71,4 +71,9 @@ export class Locators extends PythonEnvsWatchers implements I const iterators = this.locators.map((loc) => loc.iterEnvs(query)); return combineIterators(iterators); } + + public addLocator(locator: ILocator): void { + this.locators = [...this.locators, locator]; + this.addWatcher(locator); + } } diff --git a/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts b/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts index 14663e2d117d..9393934aa501 100644 --- a/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts +++ b/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts @@ -113,8 +113,13 @@ export class PythonEnvInfoCache extends PythonEnvsWatcher { const env = this.envs.splice(index, 1)[0]; this.fire({ old: env, new: undefined }); + const envPath = getEnvPath(env.executable.filename, env.location); reportInterpretersChanged([ - { path: getEnvPath(env.executable.filename, env.location).path, type: 'remove' }, + { + pathID: envPath.path, + type: 'remove', + executablePath: envPath.pathType === 'envFolderPath' ? undefined : env.executable.filename, + }, ]); }); } @@ -132,7 +137,14 @@ export class PythonEnvInfoCache extends PythonEnvsWatcher { this.fire({ old: e, new: undefined }); }); - reportInterpretersChanged([{ path: undefined, type: 'clear-all' }]); + reportInterpretersChanged([{ type: 'clear-all' }]); this.envs = []; return Promise.resolve(); } @@ -218,8 +232,9 @@ export async function createCollectionCache(storage: IPersistentStorage): Promis async function validateCache(cache: PythonEnvInfoCache) { if (isTestExecution()) { // For purposes for test execution, block on validation so that we can determinally know when it finishes. - return cache.validateCache(); + await cache.validateCache(); } // Validate in background so it doesn't block on returning the API object. - return cache.validateCache().ignoreErrors(); + cache.validateCache().ignoreErrors(); + return Promise.resolve(); } diff --git a/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts b/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts index 0e1466bc385d..b54a2a496292 100644 --- a/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts +++ b/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts @@ -47,6 +47,8 @@ export class EnvsCollectionService extends PythonEnvsWatcher | undefined { const stage = options?.stage ?? ProgressReportStage.discoveryFinished; return this.progressPromises.get(stage)?.promise; diff --git a/src/client/pythonEnvironments/base/locators/composite/envsReducer.ts b/src/client/pythonEnvironments/base/locators/composite/envsReducer.ts index 92d81243f97b..c3d3de3c7135 100644 --- a/src/client/pythonEnvironments/base/locators/composite/envsReducer.ts +++ b/src/client/pythonEnvironments/base/locators/composite/envsReducer.ts @@ -1,14 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { cloneDeep, isEqual, uniq } from 'lodash'; +import { cloneDeep, isEqual, union } from 'lodash'; import { Event, EventEmitter } from 'vscode'; import { traceVerbose } from '../../../../logging'; import { PythonEnvKind } from '../../info'; import { areSameEnv } from '../../info/env'; -import { getPrioritizedEnvKinds } from '../../info/envKind'; +import { sortExtensionSource, sortKindFunction } from '../../info/envKind'; import { BasicEnvInfo, + CompositeEnvInfo, + convertBasicToComposite, ILocator, IPythonEnvsIterator, isProgressEvent, @@ -22,15 +24,17 @@ import { PythonEnvsChangedEvent } from '../../watcher'; /** * Combines duplicate environments received from the incoming locator into one and passes on unique environments */ -export class PythonEnvsReducer implements ILocator { +export class PythonEnvsReducer implements ILocator { public get onChanged(): Event { return this.parentLocator.onChanged; } + public addNewLocator = this.parentLocator.addNewLocator; + constructor(private readonly parentLocator: ILocator) {} - public iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator { - const didUpdate = new EventEmitter | ProgressNotificationEvent>(); + public iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator { + const didUpdate = new EventEmitter | ProgressNotificationEvent>(); const incomingIterator = this.parentLocator.iterEnvs(query); const iterator = iterEnvsIterator(incomingIterator, didUpdate); iterator.onUpdated = didUpdate.event; @@ -40,13 +44,13 @@ export class PythonEnvsReducer implements ILocator { async function* iterEnvsIterator( iterator: IPythonEnvsIterator, - didUpdate: EventEmitter | ProgressNotificationEvent>, -): IPythonEnvsIterator { + didUpdate: EventEmitter | ProgressNotificationEvent>, +): IPythonEnvsIterator { const state = { done: false, pending: 0, }; - const seen: BasicEnvInfo[] = []; + const seen: CompositeEnvInfo[] = []; if (iterator.onUpdated !== undefined) { const listener = iterator.onUpdated((event) => { @@ -64,8 +68,8 @@ async function* iterEnvsIterator( ); } else if (seen[event.index] !== undefined) { const oldEnv = seen[event.index]; - seen[event.index] = event.update; - didUpdate.fire({ index: event.index, old: oldEnv, update: event.update }); + seen[event.index] = convertBasicToComposite(event.update); + didUpdate.fire({ index: event.index, old: oldEnv, update: seen[event.index] }); } else { // This implies a problem in a downstream locator traceVerbose(`Expected already iterated env, got ${event.old} (#${event.index})`); @@ -79,7 +83,7 @@ async function* iterEnvsIterator( let result = await iterator.next(); while (!result.done) { - const currEnv = result.value; + const currEnv = convertBasicToComposite(result.value); const oldIndex = seen.findIndex((s) => areSameEnv(s, currEnv)); if (oldIndex !== -1) { resolveDifferencesInBackground(oldIndex, currEnv, state, didUpdate, seen).ignoreErrors(); @@ -98,10 +102,10 @@ async function* iterEnvsIterator( async function resolveDifferencesInBackground( oldIndex: number, - newEnv: BasicEnvInfo, + newEnv: CompositeEnvInfo, state: { done: boolean; pending: number }, - didUpdate: EventEmitter | ProgressNotificationEvent>, - seen: BasicEnvInfo[], + didUpdate: EventEmitter | ProgressNotificationEvent>, + seen: CompositeEnvInfo[], ) { state.pending += 1; // It's essential we increment the pending call count before any asynchronus calls in this method. @@ -123,7 +127,7 @@ async function resolveDifferencesInBackground( */ function checkIfFinishedAndNotify( state: { done: boolean; pending: number }, - didUpdate: EventEmitter | ProgressNotificationEvent>, + didUpdate: EventEmitter | ProgressNotificationEvent>, ) { if (state.done && state.pending === 0) { didUpdate.fire({ stage: ProgressReportStage.discoveryFinished }); @@ -131,10 +135,11 @@ function checkIfFinishedAndNotify( } } -function resolveEnvCollision(oldEnv: BasicEnvInfo, newEnv: BasicEnvInfo): BasicEnvInfo { +function resolveEnvCollision(oldEnv: CompositeEnvInfo, newEnv: CompositeEnvInfo): CompositeEnvInfo { const [env] = sortEnvInfoByPriority(oldEnv, newEnv); const merged = cloneDeep(env); - merged.source = uniq((oldEnv.source ?? []).concat(newEnv.source ?? [])); + merged.source = union(oldEnv.source ?? [], newEnv.source ?? []); + merged.kind = union(merged.kind, oldEnv.kind, newEnv.kind); return merged; } @@ -142,11 +147,16 @@ function resolveEnvCollision(oldEnv: BasicEnvInfo, newEnv: BasicEnvInfo): BasicE * Selects an environment based on the environment selection priority. This should * match the priority in the environment identifier. */ -function sortEnvInfoByPriority(...envs: BasicEnvInfo[]): BasicEnvInfo[] { - // TODO: When we consolidate the PythonEnvKind and EnvironmentType we should have - // one location where we define priority. - const envKindByPriority: PythonEnvKind[] = getPrioritizedEnvKinds(); - return envs.sort( - (a: BasicEnvInfo, b: BasicEnvInfo) => envKindByPriority.indexOf(a.kind) - envKindByPriority.indexOf(b.kind), - ); +function sortEnvInfoByPriority(...envs: CompositeEnvInfo[]): CompositeEnvInfo[] { + return envs.sort((a: CompositeEnvInfo, b: CompositeEnvInfo) => { + const kindDiff = sortKindFunction(getTopKind(a.kind), getTopKind(b.kind)); + if (kindDiff !== 0) { + return kindDiff; + } + return sortExtensionSource(a.extensionId, b.extensionId); + }); +} + +function getTopKind(kinds: PythonEnvKind[]) { + return kinds.sort(sortKindFunction)[0]; } diff --git a/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts b/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts index 407d2fe12172..869d90a7aed8 100644 --- a/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts +++ b/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts @@ -3,14 +3,16 @@ import { cloneDeep } from 'lodash'; import { Event, EventEmitter } from 'vscode'; -import { identifyEnvironment } from '../../../common/environmentIdentifier'; +import { identifyEnvironment, registerIdentifier } from '../../../common/environmentIdentifier'; import { IEnvironmentInfoService } from '../../info/environmentInfoService'; -import { PythonEnvInfo } from '../../info'; +import { PythonEnvInfo, PythonEnvKind } from '../../info'; import { getEnvPath, setEnvDisplayString } from '../../info/env'; import { InterpreterInformation } from '../../info/interpreter'; import { - BasicEnvInfo, + CompositeEnvInfo, + IInternalEnvironmentProvider, ILocator, + InternalEnvironmentProviderMetadata, IPythonEnvsIterator, IResolvingLocator, isProgressEvent, @@ -20,7 +22,7 @@ import { PythonLocatorQuery, } from '../../locator'; import { PythonEnvsChangedEvent } from '../../watcher'; -import { resolveBasicEnv } from './resolverUtils'; +import { registerResolver, resolveCompositeEnv } from './resolverUtils'; import { traceVerbose, traceWarn } from '../../../../logging'; import { getEnvironmentDirFromPath, getInterpreterPathFromDir, isPythonExecutable } from '../../../common/commonUtils'; import { getEmptyVersion } from '../../info/pythonVersion'; @@ -34,8 +36,29 @@ export class PythonEnvsResolver implements IResolvingLocator { return this.parentLocator.onChanged; } + public addNewProvider(provider: IInternalEnvironmentProvider, metadata: InternalEnvironmentProviderMetadata): void { + if (this.parentLocator.addNewLocator) { + this.parentLocator.addNewLocator(provider.createLocator, metadata); + } + if (provider.envKind) { + registerIdentifier(provider.envKind, provider.canIdentifyEnvironment, metadata.extensionId); + registerResolver(provider.envKind, provider.getEnvironmentDetails, metadata.extensionId); + } else { + registerIdentifier( + metadata.extensionId as PythonEnvKind, + provider.canIdentifyEnvironment, + metadata.extensionId, + ); + registerResolver( + metadata.extensionId as PythonEnvKind, + provider.getEnvironmentDetails, + metadata.extensionId, + ); + } + } + constructor( - private readonly parentLocator: ILocator, + private readonly parentLocator: ILocator, private readonly environmentInfoService: IEnvironmentInfoService, ) {} @@ -43,7 +66,10 @@ export class PythonEnvsResolver implements IResolvingLocator { const [executablePath, envPath] = await getExecutablePathAndEnvPath(path); path = executablePath.length ? executablePath : envPath; const kind = await identifyEnvironment(path); - const environment = await resolveBasicEnv({ kind, executablePath, envPath }); + const environment = await resolveCompositeEnv({ kind: [kind], executablePath, envPath }); + if (!environment) { + return undefined; + } const info = await this.environmentInfoService.getEnvironmentInfo(environment); if (!info) { return undefined; @@ -60,7 +86,7 @@ export class PythonEnvsResolver implements IResolvingLocator { } private async *iterEnvsIterator( - iterator: IPythonEnvsIterator, + iterator: IPythonEnvsIterator, didUpdate: EventEmitter, ): IPythonEnvsIterator { const state = { @@ -86,8 +112,11 @@ export class PythonEnvsResolver implements IResolvingLocator { ); } else if (seen[event.index] !== undefined) { const old = seen[event.index]; - seen[event.index] = await resolveBasicEnv(event.update, true); - didUpdate.fire({ old, index: event.index, update: seen[event.index] }); + const env = await resolveCompositeEnv(event.update, true); + didUpdate.fire({ old, index: event.index, update: env }); + if (env) { + seen[event.index] = env; + } this.resolveInBackground(event.index, state, didUpdate, seen).ignoreErrors(); } else { // This implies a problem in a downstream locator @@ -103,11 +132,13 @@ export class PythonEnvsResolver implements IResolvingLocator { let result = await iterator.next(); while (!result.done) { // Use cache from the current refresh where possible. - const currEnv = await resolveBasicEnv(result.value, true); - seen.push(currEnv); - yield currEnv; - this.resolveInBackground(seen.indexOf(currEnv), state, didUpdate, seen).ignoreErrors(); - result = await iterator.next(); + const currEnv = await resolveCompositeEnv(result.value, true); + if (currEnv) { + seen.push(currEnv); + yield currEnv; + this.resolveInBackground(seen.indexOf(currEnv), state, didUpdate, seen).ignoreErrors(); + result = await iterator.next(); + } } if (iterator.onUpdated === undefined) { state.done = true; diff --git a/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts b/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts index c41c52510280..fa0a87421357 100644 --- a/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts +++ b/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts @@ -23,34 +23,72 @@ import { getPyenvVersionsDir, parsePyenvVersion } from '../../../common/environm import { Architecture, getOSType, OSType } from '../../../../common/utils/platform'; import { getPythonVersionFromPath as parsePythonVersionFromPath, parseVersion } from '../../info/pythonVersion'; import { getRegistryInterpreters, getRegistryInterpretersSync } from '../../../common/windowsUtils'; -import { BasicEnvInfo } from '../../locator'; +import { CompositeEnvInfo } from '../../locator'; import { parseVersionFromExecutable } from '../../info/executable'; import { traceError, traceWarn } from '../../../../logging'; +import { sortExtensionSource } from '../../info/envKind'; -function getResolvers(): Map Promise> { - const resolvers = new Map Promise>(); - Object.values(PythonEnvKind).forEach((k) => { - resolvers.set(k, resolveGloballyInstalledEnv); - }); - virtualEnvKinds.forEach((k) => { - resolvers.set(k, resolveSimpleEnv); - }); - resolvers.set(PythonEnvKind.Conda, resolveCondaEnv); - resolvers.set(PythonEnvKind.MicrosoftStore, resolveMicrosoftStoreEnv); - resolvers.set(PythonEnvKind.Pyenv, resolvePyenvEnv); - return resolvers; +type ResolverType = (_: CompositeEnvInfo, useCache?: boolean) => Promise; +type ResolversType = { resolver: ResolverType; extensionId?: string }[]; +const resolvers = new Map(); +Object.values(PythonEnvKind).forEach((k) => { + resolvers.set(k, resolveGloballyInstalledEnv); +}); +virtualEnvKinds.forEach((k) => { + resolvers.set(k, resolveSimpleEnv); +}); +resolvers.set(PythonEnvKind.Conda, resolveCondaEnv); +resolvers.set(PythonEnvKind.MicrosoftStore, resolveMicrosoftStoreEnv); +resolvers.set(PythonEnvKind.Pyenv, resolvePyenvEnv); + +export function registerResolver( + kind: PythonEnvKind, + resolver: (_: CompositeEnvInfo, useCache?: boolean) => Promise, + extensionId: string, +): void { + const resolversForKind = resolvers.get(kind); + if (!resolversForKind) { + resolvers.set(kind, resolver); + } else if (Array.isArray(resolversForKind)) { + resolversForKind.push({ resolver, extensionId }); + resolvers.set(kind, resolversForKind); + } else { + resolvers.set(kind, [{ resolver, extensionId }, { resolver: resolversForKind }]); + } + resolvers.set(kind, resolver); } /** - * Find as much info about the given Basic Python env as possible without running the + * Find as much info about the given Python env as possible without running the * executable and returns it. Notice `undefined` is never returned, so environment * returned could still be invalid. */ -export async function resolveBasicEnv(env: BasicEnvInfo, useCache = false): Promise { +export async function resolveCompositeEnv(env: CompositeEnvInfo, useCache = false): Promise { const { kind, source } = env; - const resolvers = getResolvers(); - const resolverForKind = resolvers.get(kind)!; + let value = resolvers.get(kind[0]); + if (!value) { + value = env.extensionId ? resolvers.get(env.extensionId as PythonEnvKind) : undefined; + if (!value) { + traceError('No resolver found for env:', JSON.stringify(env)); + return undefined; + } + } + let resolverForKind: ResolverType; + if (Array.isArray(value)) { + const resolver = env.extensionId + ? value.find((v) => v.extensionId === env.extensionId)?.resolver + : value.sort((a, b) => sortExtensionSource(a.extensionId, b.extensionId))[0].resolver; + if (!resolver) { + return undefined; + } + resolverForKind = resolver; + } else { + resolverForKind = value; + } const resolvedEnv = await resolverForKind(env, useCache); + if (!resolvedEnv) { + return undefined; + } resolvedEnv.searchLocation = getSearchLocation(resolvedEnv); resolvedEnv.source = uniq(resolvedEnv.source.concat(source ?? [])); if (getOSType() === OSType.Windows && resolvedEnv.source?.includes(PythonEnvSource.WindowsRegistry)) { @@ -98,7 +136,7 @@ async function updateEnvUsingRegistry(env: PythonEnvInfo): Promise { } catch (ex) { version = UNKNOWN_PYTHON_VERSION; } - env.kind = env.kind === PythonEnvKind.Unknown ? PythonEnvKind.OtherGlobal : env.kind; + env.kind = [env.kind[0] === PythonEnvKind.Unknown ? PythonEnvKind.OtherGlobal : env.kind[0]]; env.version = comparePythonVersionSpecificity(version, env.version) > 0 ? version : env.version; env.distro.defaultDisplayName = data.companyDisplayName; env.arch = data.bitnessStr === '32bit' ? Architecture.x86 : Architecture.x64; @@ -109,7 +147,7 @@ async function updateEnvUsingRegistry(env: PythonEnvInfo): Promise { } } -async function resolveGloballyInstalledEnv(env: BasicEnvInfo): Promise { +async function resolveGloballyInstalledEnv(env: CompositeEnvInfo): Promise { const { executablePath } = env; let version; try { @@ -125,7 +163,7 @@ async function resolveGloballyInstalledEnv(env: BasicEnvInfo): Promise { +async function resolveSimpleEnv(env: CompositeEnvInfo): Promise { const { executablePath, kind } = env; const envInfo = buildEnvInfo({ kind, @@ -138,7 +176,7 @@ async function resolveSimpleEnv(env: BasicEnvInfo): Promise { return envInfo; } -async function resolveCondaEnv(env: BasicEnvInfo, useCache?: boolean): Promise { +async function resolveCondaEnv(env: CompositeEnvInfo, useCache?: boolean): Promise { const { executablePath } = env; const conda = await Conda.getConda(); if (conda === undefined) { @@ -147,7 +185,7 @@ async function resolveCondaEnv(env: BasicEnvInfo, useCache?: boolean): Promise

0) { executable = env.executablePath; @@ -156,7 +194,7 @@ async function resolveCondaEnv(env: BasicEnvInfo, useCache?: boolean): Promise

{ +async function resolvePyenvEnv(env: CompositeEnvInfo): Promise { const { executablePath } = env; const location = getEnvironmentDirFromPath(executablePath); const name = path.basename(location); @@ -188,7 +226,7 @@ async function resolvePyenvEnv(env: BasicEnvInfo): Promise { const versionStrings = parsePyenvVersion(name); const envInfo = buildEnvInfo({ - kind: PythonEnvKind.Pyenv, + kind: env.kind, executable: executablePath, source: [], location, @@ -231,10 +269,10 @@ async function isBaseCondaPyenvEnvironment(executablePath: string) { return arePathsSame(path.dirname(location), pyenvVersionDir); } -async function resolveMicrosoftStoreEnv(env: BasicEnvInfo): Promise { +async function resolveMicrosoftStoreEnv(env: CompositeEnvInfo): Promise { const { executablePath } = env; return buildEnvInfo({ - kind: PythonEnvKind.MicrosoftStore, + kind: env.kind, executable: executablePath, version: parsePythonVersionFromPath(executablePath), org: 'Microsoft', diff --git a/src/client/pythonEnvironments/base/locators/wrappers.ts b/src/client/pythonEnvironments/base/locators/wrappers.ts index 9313ade20218..653cf2c918dd 100644 --- a/src/client/pythonEnvironments/base/locators/wrappers.ts +++ b/src/client/pythonEnvironments/base/locators/wrappers.ts @@ -7,33 +7,58 @@ import { IDisposable } from '../../../common/types'; import { iterEmpty } from '../../../common/utils/async'; import { getURIFilter } from '../../../common/utils/misc'; import { Disposables } from '../../../common/utils/resourceLifecycle'; +import { ConvertLocator } from '../../converter'; import { PythonEnvInfo } from '../info'; -import { ILocator, IPythonEnvsIterator, PythonLocatorQuery } from '../locator'; +import { + BasicEnvInfo, + ILocator, + INonWorkspaceLocatorFactory, + IPythonEnvsIterator, + PythonLocatorQuery, + IWorkspaceLocatorFactory, + ILocatorRegister, + ILocatorFactory, + InternalEnvironmentProviderMetadata, +} from '../locator'; import { combineIterators, Locators } from '../locators'; import { LazyResourceBasedLocator } from './common/resourceBasedLocator'; +function IsNonWorkspaceLocatorFactory( + pet: IWorkspaceLocatorFactory | INonWorkspaceLocatorFactory, +): pet is INonWorkspaceLocatorFactory { + return pet.length === 0; +} + /** * A wrapper around all locators used by the extension. */ -export class ExtensionLocators extends Locators { +export class ExtensionLocators extends Locators implements ILocatorRegister { constructor( // These are expected to be low-level locators (e.g. system). - private readonly nonWorkspace: ILocator[], + private nonWorkspace: ILocator[], // This is expected to be a locator wrapping any found in // the workspace (i.e. WorkspaceLocators). - private readonly workspace: ILocator, + private workspace: WorkspaceLocators, ) { super([...nonWorkspace, workspace]); } - public iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator { - const iterators: IPythonEnvsIterator[] = [this.workspace.iterEnvs(query)]; + public iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator { + const iterators: IPythonEnvsIterator[] = [this.workspace.iterEnvs(query)]; if (!query?.searchLocations?.doNotIncludeNonRooted) { iterators.push(...this.nonWorkspace.map((loc) => loc.iterEnvs(query))); } return combineIterators(iterators); } + + public addNewLocator(locatorFactory: ILocatorFactory, metadata: InternalEnvironmentProviderMetadata): void { + if (IsNonWorkspaceLocatorFactory(locatorFactory)) { + this.nonWorkspace = [...this.nonWorkspace, new ConvertLocator(locatorFactory(), metadata)]; + } else { + this.workspace.addNewLocator(locatorFactory, metadata); + } + } } type WorkspaceLocatorFactoryResult = ILocator & Partial; type WorkspaceLocatorFactory = (root: Uri) => WorkspaceLocatorFactoryResult[]; @@ -52,12 +77,15 @@ type WatchRootsFunc = (args: WatchRootsArgs) => IDisposable; * The factories are used to produce the locators for each workspace folder. */ -export class WorkspaceLocators extends LazyResourceBasedLocator { - private readonly locators: Record, IDisposable]> = {}; +export class WorkspaceLocators extends LazyResourceBasedLocator { + private readonly locators: Record, IDisposable]> = {}; private readonly roots: Record = {}; - constructor(private readonly watchRoots: WatchRootsFunc, private readonly factories: WorkspaceLocatorFactory[]) { + constructor( + private readonly watchRoots: WatchRootsFunc, + private readonly factory: WorkspaceLocatorFactory, + ) { super(); } @@ -69,7 +97,7 @@ export class WorkspaceLocators extends LazyResourceBasedLocat roots.forEach((root) => this.removeRoot(root)); } - protected doIterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator { + protected doIterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator { const iterators = Object.keys(this.locators).map((key) => { if (query?.searchLocations !== undefined) { const root = this.roots[key]; @@ -78,7 +106,7 @@ export class WorkspaceLocators extends LazyResourceBasedLocat // Ignore any requests for global envs. if (!query.searchLocations.roots.some(filter)) { // This workspace folder did not match the query, so skip it! - return iterEmpty(); + return iterEmpty(); } } // The query matches or was not location-specific. @@ -107,15 +135,13 @@ export class WorkspaceLocators extends LazyResourceBasedLocat private addRoot(root: Uri): void { // Create the root's locator, wrapping each factory-generated locator. - const locators: ILocator[] = []; + const locators: ILocator[] = []; const disposables = new Disposables(); - this.factories.forEach((create) => { - create(root).forEach((loc) => { - locators.push(loc); - if (loc.dispose !== undefined) { - disposables.push(loc as IDisposable); - } - }); + this.factory(root).forEach((loc) => { + locators.push(loc); + if (loc.dispose !== undefined) { + disposables.push(loc as IDisposable); + } }); const locator = new Locators(locators); // Cache it. @@ -133,6 +159,19 @@ export class WorkspaceLocators extends LazyResourceBasedLocat ); } + public addNewLocator( + locatorFactory: IWorkspaceLocatorFactory, + metadata: InternalEnvironmentProviderMetadata, + ): void { + Object.keys(this.roots).forEach((key) => { + const root = this.roots[key]; + const newLocator = locatorFactory(root.fsPath); + const convertedLocator: ILocator = new ConvertLocator(newLocator, metadata); + const [locators] = this.locators[key]; + locators.addLocator(convertedLocator); + }); + } + private removeRoot(root: Uri): void { const key = root.toString(); const found = this.locators[key]; diff --git a/src/client/pythonEnvironments/base/watchers.ts b/src/client/pythonEnvironments/base/watchers.ts index 60bf5f7516da..a72b66d61ed9 100644 --- a/src/client/pythonEnvironments/base/watchers.ts +++ b/src/client/pythonEnvironments/base/watchers.ts @@ -30,4 +30,10 @@ export class PythonEnvsWatchers implements IPythonEnvsWatcher, IDisposable { public async dispose(): Promise { await this.disposables.dispose(); } + + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + protected addWatcher(w: IPythonEnvsWatcher) { + const disposable = w.onChanged((e) => this.watcher.fire(e)); + this.disposables.push(disposable); + } } diff --git a/src/client/pythonEnvironments/common/environmentIdentifier.ts b/src/client/pythonEnvironments/common/environmentIdentifier.ts index 957321ed8e61..2325066a1d03 100644 --- a/src/client/pythonEnvironments/common/environmentIdentifier.ts +++ b/src/client/pythonEnvironments/common/environmentIdentifier.ts @@ -3,7 +3,7 @@ import { traceWarn } from '../../logging'; import { PythonEnvKind } from '../base/info'; -import { getPrioritizedEnvKinds } from '../base/info/envKind'; +import { getPrioritizedEnvKinds, sortExtensionSource } from '../base/info/envKind'; import { isCondaEnvironment } from './environmentManagers/conda'; import { isGloballyInstalledEnv } from './environmentManagers/globalInstalledEnvs'; import { isPipenvEnvironment } from './environmentManagers/pipenv'; @@ -16,45 +16,63 @@ import { } from './environmentManagers/simplevirtualenvs'; import { isMicrosoftStoreEnvironment } from './environmentManagers/microsoftStoreEnv'; -function getIdentifiers(): Map Promise> { - const notImplemented = () => Promise.resolve(false); - const defaultTrue = () => Promise.resolve(true); - const identifier: Map Promise> = new Map(); - Object.values(PythonEnvKind).forEach((k) => { - identifier.set(k, notImplemented); - }); +type IdentifierType = (path: string) => Promise; +type IdentifiersType = { identifier: IdentifierType; extensionId?: string }; +const identifiers: Map = new Map(); - identifier.set(PythonEnvKind.Conda, isCondaEnvironment); - identifier.set(PythonEnvKind.MicrosoftStore, isMicrosoftStoreEnvironment); - identifier.set(PythonEnvKind.Pipenv, isPipenvEnvironment); - identifier.set(PythonEnvKind.Pyenv, isPyenvEnvironment); - identifier.set(PythonEnvKind.Poetry, isPoetryEnvironment); - identifier.set(PythonEnvKind.Venv, isVenvEnvironment); - identifier.set(PythonEnvKind.VirtualEnvWrapper, isVirtualEnvWrapperEnvironment); - identifier.set(PythonEnvKind.VirtualEnv, isVirtualEnvEnvironment); - identifier.set(PythonEnvKind.Unknown, defaultTrue); - identifier.set(PythonEnvKind.OtherGlobal, isGloballyInstalledEnv); - return identifier; +export function registerIdentifier(kind: PythonEnvKind, identifier: IdentifierType, extensionId: string): void { + const identifiersForKind = identifiers.get(kind); + if (!identifiersForKind) { + identifiers.set(kind, identifier); + } else if (Array.isArray(identifiersForKind)) { + identifiersForKind.push({ identifier, extensionId }); + identifiers.set(kind, identifiersForKind); + } else { + identifiers.set(kind, [{ identifier, extensionId }, { identifier: identifiersForKind }]); + } } +const notImplemented = () => Promise.resolve(false); +const defaultTrue = () => Promise.resolve(true); +Object.values(PythonEnvKind).forEach((k) => { + identifiers.set(k, notImplemented); +}); + +identifiers.set(PythonEnvKind.Conda, isCondaEnvironment); +identifiers.set(PythonEnvKind.MicrosoftStore, isMicrosoftStoreEnvironment); +identifiers.set(PythonEnvKind.Pipenv, isPipenvEnvironment); +identifiers.set(PythonEnvKind.Pyenv, isPyenvEnvironment); +identifiers.set(PythonEnvKind.Poetry, isPoetryEnvironment); +identifiers.set(PythonEnvKind.Venv, isVenvEnvironment); +identifiers.set(PythonEnvKind.VirtualEnvWrapper, isVirtualEnvWrapperEnvironment); +identifiers.set(PythonEnvKind.VirtualEnv, isVirtualEnvEnvironment); +identifiers.set(PythonEnvKind.Unknown, defaultTrue); +identifiers.set(PythonEnvKind.OtherGlobal, isGloballyInstalledEnv); + /** * Returns environment type. * @param {string} path : Absolute path to the python interpreter binary or path to environment. * @returns {PythonEnvKind} */ export async function identifyEnvironment(path: string): Promise { - const identifiers = getIdentifiers(); const prioritizedEnvTypes = getPrioritizedEnvKinds(); for (const e of prioritizedEnvTypes) { - const identifier = identifiers.get(e); - if ( - identifier && - (await identifier(path).catch((ex) => { - traceWarn(`Identifier for ${e} failed to identify ${path}`, ex); - return false; - })) - ) { - return e; + const value = identifiers.get(e); + if (value) { + let identifier: IdentifierType; + if (Array.isArray(value)) { + identifier = value.sort((a, b) => sortExtensionSource(a.extensionId, b.extensionId))[0].identifier; + } else { + identifier = value; + } + if ( + await identifier(path).catch((ex) => { + traceWarn(`Identifier for ${e} failed to identify ${path}`, ex); + return false; + }) + ) { + return e; + } } } return PythonEnvKind.Unknown; diff --git a/src/client/pythonEnvironments/converter.ts b/src/client/pythonEnvironments/converter.ts new file mode 100644 index 000000000000..5cab4b803059 --- /dev/null +++ b/src/client/pythonEnvironments/converter.ts @@ -0,0 +1,194 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { EventEmitter, Event, Uri } from 'vscode'; +import { FileChangeType } from '../common/platform/fileSystemWatcher'; +import { traceVerbose } from '../logging'; +import { PythonEnvInfo, PythonEnvKind } from './base/info'; +import { buildEnvInfo } from './base/info/env'; +import { + ILocator, + BasicEnvInfo, + IPythonEnvsIterator, + PythonEnvUpdatedEvent, + ProgressNotificationEvent, + isProgressEvent, + ProgressReportStage, + InternalDetailsAPI, + ProposedDetailsAPI, + IResolverAPI, + IInternalResolverAPI, + IInternalEnvironmentProvider, + IEnvironmentProvider, + EnvironmentProviderMetadata, + InternalEnvironmentProviderMetadata, + EnvChangeType, + EnvInfo, + ILocatorAPI, + LocatorEnvsChangedEvent, +} from './base/locator'; +import { PythonEnvsChangedEvent } from './base/watcher'; + +export function convertProviderAPI(proposed: IEnvironmentProvider): IInternalEnvironmentProvider { + return { + createLocator: proposed.createLocator, + ...convertResolverAPI(proposed), + }; +} + +export function convertProviderMetaData(proposed: EnvironmentProviderMetadata): InternalEnvironmentProviderMetadata { + return { + extensionId: proposed.extensionId, + environments: { + envKinds: proposed.environments?.envSources?.map((e) => convertKind(e)) ?? [PythonEnvKind.Unknown], + }, + }; +} + +function convertResolverAPI(proposed: IResolverAPI): IInternalResolverAPI { + return { + envKind: proposed.envSource ? convertKind(proposed.envSource) : undefined, + canIdentifyEnvironment: proposed.canIdentifyEnvironment, + getEnvironmentDetails: convertDetailsAPI(proposed.getEnvironmentDetails), + }; +} + +function convertDetailsAPI(proposed: ProposedDetailsAPI): InternalDetailsAPI { + return async (env: BasicEnvInfo): Promise => { + const details = await proposed({ executablePath: env.executablePath, envPath: env.envPath }); + if (!details) { + return undefined; + } + const envInfo = buildEnvInfo({ + kind: details.environment?.source.map((k) => convertKind(k)), + version: details.version, + executable: details.executable.path, + arch: details.executable.bitness, + sysPrefix: details.executable.sysPrefix, + }); + return envInfo; + }; +} + +function convertKind(source: string): PythonEnvKind { + return (source as unknown) as PythonEnvKind; +} + +/** + * Converts the proposed interface into a class implementing basic interface. + * ILocator ======> ILocator + */ +export class ConvertLocator implements ILocator { + private readonly didChange = new EventEmitter(); + + private eventKeys: Record = { + add: FileChangeType.Created, + remove: FileChangeType.Deleted, + update: FileChangeType.Changed, + }; + + public get onChanged(): Event { + return this.didChange.event; + } + + constructor( + private readonly parentLocator: ILocatorAPI, + private readonly metadata: InternalEnvironmentProviderMetadata, + ) { + if (parentLocator.onChanged) { + parentLocator.onChanged((e: LocatorEnvsChangedEvent) => { + const event: PythonEnvsChangedEvent = { + type: this.eventKeys[`${e.type}`], + kind: e.env?.envSources ? convertKind(e.env?.envSources[0]) : undefined, + searchLocation: Uri.file(e.pathId), + }; + this.didChange.fire(event); + }); + } + } + + public iterEnvs(): IPythonEnvsIterator { + const didUpdate = new EventEmitter | ProgressNotificationEvent>(); + const incomingIterator = this.parentLocator.iterEnvs!(); + const iterator = this.iterEnvsIterator(incomingIterator, didUpdate); + iterator.onUpdated = didUpdate.event; + return iterator; + } + + private async *iterEnvsIterator( + iterator: IPythonEnvsIterator, + didUpdate: EventEmitter | ProgressNotificationEvent>, + ): IPythonEnvsIterator { + const state = { + done: false, + pending: 0, + }; + const seen: BasicEnvInfo[] = []; + + if (iterator.onUpdated !== undefined) { + const listener = iterator.onUpdated((event) => { + state.pending += 1; + if (isProgressEvent(event)) { + if (event.stage === ProgressReportStage.discoveryFinished) { + state.done = true; + listener.dispose(); + } else { + didUpdate.fire(event); + } + } else if (event.update === undefined) { + throw new Error( + 'Unsupported behavior: `undefined` environment updates are not supported from downstream locators in reducer', + ); + } else if (seen[event.index] !== undefined) { + const oldEnv = seen[event.index]; + seen[event.index] = this.convertToBasicEnv(event.update); + didUpdate.fire({ index: event.index, old: oldEnv, update: this.convertToBasicEnv(event.update) }); + } else { + // This implies a problem in a downstream locator + traceVerbose(`Expected already iterated env, got ${event.old} (#${event.index})`); + } + state.pending -= 1; + checkIfFinishedAndNotify(state, didUpdate); + }); + } else { + didUpdate.fire({ stage: ProgressReportStage.discoveryStarted }); + } + + let result = await iterator.next(); + while (!result.done) { + const currEnv = this.convertToBasicEnv(result.value); + yield currEnv; + seen.push(currEnv); + result = await iterator.next(); + } + if (iterator.onUpdated === undefined) { + state.done = true; + checkIfFinishedAndNotify(state, didUpdate); + } + } + + private convertToBasicEnv(env: EnvInfo): BasicEnvInfo { + const sources = Array.isArray(env.envSource) ? env.envSource : [env.envSource]; + return { + executablePath: env.executablePath, + envPath: env.envPath, + kind: sources.map((s) => convertKind(s)), + extensionId: this.metadata.extensionId, + }; + } +} + +/** + * When all info from incoming iterator has been received and all background calls finishes, notify that we're done + * @param state Carries the current state of progress + * @param didUpdate Used to notify when finished + */ +function checkIfFinishedAndNotify( + state: { done: boolean; pending: number }, + didUpdate: EventEmitter | ProgressNotificationEvent>, +) { + if (state.done && state.pending === 0) { + didUpdate.fire({ stage: ProgressReportStage.discoveryFinished }); + didUpdate.dispose(); + } +} diff --git a/src/client/pythonEnvironments/index.ts b/src/client/pythonEnvironments/index.ts index b4e9d45fa44a..32e959007ca4 100644 --- a/src/client/pythonEnvironments/index.ts +++ b/src/client/pythonEnvironments/index.ts @@ -106,7 +106,7 @@ async function createLocator( // This is shared. ): Promise { // Create the low-level locators. - let locators: ILocator = new ExtensionLocators( + const locators: ILocator = new ExtensionLocators( // Here we pull the locators together. createNonWorkspaceLocators(ext), createWorkspaceLocator(ext), @@ -116,9 +116,9 @@ async function createLocator( const envInfoService = getEnvironmentInfoService(ext.disposables); // Build the stack of composite locators. - locators = new PythonEnvsReducer(locators); + const reducer = new PythonEnvsReducer(locators); const resolvingLocator = new PythonEnvsResolver( - locators, + reducer, // These are shared. envInfoService, ); @@ -177,10 +177,10 @@ function watchRoots(args: WatchRootsArgs): IDisposable { }); } -function createWorkspaceLocator(ext: ExtensionState): WorkspaceLocators { - const locators = new WorkspaceLocators(watchRoots, [ - (root: vscode.Uri) => [new WorkspaceVirtualEnvironmentLocator(root.fsPath), new PoetryLocator(root.fsPath)], - // Add an ILocator factory func here for each kind of workspace-rooted locator. +function createWorkspaceLocator(ext: ExtensionState): WorkspaceLocators { + const locators = new WorkspaceLocators(watchRoots, (root: vscode.Uri) => [ + new WorkspaceVirtualEnvironmentLocator(root.fsPath), + new PoetryLocator(root.fsPath), ]); ext.disposables.push(locators); return locators; diff --git a/src/client/pythonEnvironments/info/index.ts b/src/client/pythonEnvironments/info/index.ts index d0f41c45a5b1..aeb04f988ad8 100644 --- a/src/client/pythonEnvironments/info/index.ts +++ b/src/client/pythonEnvironments/info/index.ts @@ -10,17 +10,21 @@ import { PythonVersion } from './pythonVersion'; * The supported Python environment types. */ export enum EnvironmentType { - Unknown = 'Unknown', Conda = 'Conda', - VirtualEnv = 'VirtualEnv', + // Complex virtual envs Pipenv = 'PipEnv', - Pyenv = 'Pyenv', - Venv = 'Venv', - MicrosoftStore = 'MicrosoftStore', Poetry = 'Poetry', + // Simple virtual envs + VirtualEnv = 'VirtualEnv', + Venv = 'Venv', VirtualEnvWrapper = 'VirtualEnvWrapper', + // Global Global = 'Global', System = 'System', + MicrosoftStore = 'MicrosoftStore', + Unknown = 'Unknown', + // Pyenv + Pyenv = 'Pyenv', } export const virtualEnvTypes = [ diff --git a/src/client/pythonEnvironments/legacyIOC.ts b/src/client/pythonEnvironments/legacyIOC.ts index 68a61f93a7a2..8797e7febdd5 100644 --- a/src/client/pythonEnvironments/legacyIOC.ts +++ b/src/client/pythonEnvironments/legacyIOC.ts @@ -51,7 +51,7 @@ function convertEnvInfo(info: PythonEnvInfo): PythonEnvironment { architecture: arch, }; - const envType = convertedKinds.get(kind); + const envType = convertedKinds.get(kind[0]); if (envType !== undefined) { env.envType = envType; } diff --git a/src/test/pythonEnvironments/base/common.ts b/src/test/pythonEnvironments/base/common.ts index 603f423037bc..5193a45dbb07 100644 --- a/src/test/pythonEnvironments/base/common.ts +++ b/src/test/pythonEnvironments/base/common.ts @@ -17,7 +17,7 @@ import { import { buildEnvInfo } from '../../../client/pythonEnvironments/base/info/env'; import { getEmptyVersion, parseVersion } from '../../../client/pythonEnvironments/base/info/pythonVersion'; import { - BasicEnvInfo, + CompositeEnvInfo, IPythonEnvsIterator, isProgressEvent, Locator, @@ -53,7 +53,7 @@ export function createLocatedEnv( ? getEmptyVersion() // an empty version : parseVersion(versionStr); const env = buildEnvInfo({ - kind, + kind: [kind], executable, location, version, @@ -71,8 +71,8 @@ export function createBasicEnv( executablePath: string, source?: PythonEnvSource[], envPath?: string, -): BasicEnvInfo { - const basicEnv = { executablePath, kind, source, envPath }; +): CompositeEnvInfo { + const basicEnv = { executablePath, kind: [kind], source, envPath }; if (!source) { delete basicEnv.source; } diff --git a/src/test/pythonEnvironments/base/locators/composite/envsResolver.unit.test.ts b/src/test/pythonEnvironments/base/locators/composite/envsResolver.unit.test.ts index 6cd6d53330a5..b77178f2bc27 100644 --- a/src/test/pythonEnvironments/base/locators/composite/envsResolver.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/composite/envsResolver.unit.test.ts @@ -18,7 +18,7 @@ import { } from '../../../../../client/pythonEnvironments/base/info'; import { getEmptyVersion, parseVersion } from '../../../../../client/pythonEnvironments/base/info/pythonVersion'; import { - BasicEnvInfo, + CompositeEnvInfo, isProgressEvent, ProgressNotificationEvent, ProgressReportStage, @@ -80,7 +80,7 @@ suite('Python envs locator - Environments Resolver', () => { return { name, location, - kind, + kind: [kind], executable: { filename: interpreterPath, sysPrefix: '', @@ -130,7 +130,7 @@ suite('Python envs locator - Environments Resolver', () => { "Python ('win1': venv)", ); const envsReturnedByParentLocator = [env1]; - const parentLocator = new SimpleLocator(envsReturnedByParentLocator); + const parentLocator = new SimpleLocator(envsReturnedByParentLocator); const resolver = new PythonEnvsResolver(parentLocator, envInfoService); const iterator = resolver.iterEnvs(); @@ -153,7 +153,7 @@ suite('Python envs locator - Environments Resolver', () => { path.join(testVirtualHomeDir, '.venvs', 'win1'), ); const envsReturnedByParentLocator = [env1]; - const parentLocator = new SimpleLocator(envsReturnedByParentLocator); + const parentLocator = new SimpleLocator(envsReturnedByParentLocator); const resolver = new PythonEnvsResolver(parentLocator, envInfoService); const iterator = resolver.iterEnvs(); @@ -179,7 +179,7 @@ suite('Python envs locator - Environments Resolver', () => { path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), ); const envsReturnedByParentLocator = [env1]; - const parentLocator = new SimpleLocator(envsReturnedByParentLocator); + const parentLocator = new SimpleLocator(envsReturnedByParentLocator); const resolver = new PythonEnvsResolver(parentLocator, envInfoService); // Act @@ -208,8 +208,8 @@ suite('Python envs locator - Environments Resolver', () => { path.join(testVirtualHomeDir, '.venvs', 'win1'), ); const envsReturnedByParentLocator = [env]; - const didUpdate = new EventEmitter | ProgressNotificationEvent>(); - const parentLocator = new SimpleLocator(envsReturnedByParentLocator, { + const didUpdate = new EventEmitter | ProgressNotificationEvent>(); + const parentLocator = new SimpleLocator(envsReturnedByParentLocator, { onUpdated: didUpdate.event, }); const resolver = new PythonEnvsResolver(parentLocator, envInfoService); @@ -254,8 +254,8 @@ suite('Python envs locator - Environments Resolver', () => { path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), ); const envsReturnedByParentLocator = [env]; - const didUpdate = new EventEmitter | ProgressNotificationEvent>(); - const parentLocator = new SimpleLocator(envsReturnedByParentLocator, { + const didUpdate = new EventEmitter | ProgressNotificationEvent>(); + const parentLocator = new SimpleLocator(envsReturnedByParentLocator, { onUpdated: didUpdate.event, }); const resolver = new PythonEnvsResolver(parentLocator, envInfoService); diff --git a/src/test/pythonEnvironments/base/locators/composite/resolverUtils.unit.test.ts b/src/test/pythonEnvironments/base/locators/composite/resolverUtils.unit.test.ts index 893f8bed7655..c59a5cac95f5 100644 --- a/src/test/pythonEnvironments/base/locators/composite/resolverUtils.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/composite/resolverUtils.unit.test.ts @@ -24,7 +24,12 @@ import { AnacondaCompanyName, CondaInfo, } from '../../../../../client/pythonEnvironments/common/environmentManagers/conda'; -import { resolveBasicEnv } from '../../../../../client/pythonEnvironments/base/locators/composite/resolverUtils'; +import { resolveCompositeEnv } from '../../../../../client/pythonEnvironments/base/locators/composite/resolverUtils'; +import { BasicEnvInfo, convertBasicToComposite } from '../../../../../client/pythonEnvironments/base/locator'; + +function resolveEnv(env: BasicEnvInfo) { + return resolveCompositeEnv(convertBasicToComposite(env)); +} suite('Resolver Utils', () => { let getWorkspaceFolders: sinon.SinonStub; @@ -87,7 +92,7 @@ suite('Resolver Utils', () => { const executablePath = path.join(testPyenvVersionsDir, '3.9.0', 'bin', 'python'); const expected = getExpectedPyenvInfo1(); - const actual = await resolveBasicEnv({ executablePath, kind: PythonEnvKind.Pyenv }); + const actual = await resolveEnv({ executablePath, kind: PythonEnvKind.Pyenv }); assertEnvEqual(actual, expected); }); @@ -96,7 +101,7 @@ suite('Resolver Utils', () => { const executablePath = path.join(testPyenvVersionsDir, 'miniconda3-4.7.12', 'bin', 'python'); const expected = getExpectedPyenvInfo2(); - const actual = await resolveBasicEnv({ executablePath, kind: PythonEnvKind.Pyenv }); + const actual = await resolveEnv({ executablePath, kind: PythonEnvKind.Pyenv }); assertEnvEqual(actual, expected); }); }); @@ -147,14 +152,14 @@ suite('Resolver Utils', () => { searchLocation: undefined, name: '', location: '', - kind: PythonEnvKind.MicrosoftStore, + kind: [PythonEnvKind.MicrosoftStore], distro: { org: 'Microsoft' }, source: [PythonEnvSource.PathEnvVar], ...createExpectedInterpreterInfo(python38path), }; setEnvDisplayString(expected); - const actual = await resolveBasicEnv({ + const actual = await resolveEnv({ executablePath: python38path, kind: PythonEnvKind.MicrosoftStore, }); @@ -169,14 +174,14 @@ suite('Resolver Utils', () => { searchLocation: undefined, name: '', location: '', - kind: PythonEnvKind.MicrosoftStore, + kind: [PythonEnvKind.MicrosoftStore], distro: { org: 'Microsoft' }, source: [PythonEnvSource.PathEnvVar], ...createExpectedInterpreterInfo(python38path), }; setEnvDisplayString(expected); - const actual = await resolveBasicEnv({ + const actual = await resolveEnv({ executablePath: python38path, kind: PythonEnvKind.MicrosoftStore, }); @@ -223,7 +228,7 @@ suite('Resolver Utils', () => { const info: PythonEnvInfo = { name, location, - kind, + kind: [kind], executable: { filename: interpreterPath, sysPrefix: '', @@ -253,7 +258,7 @@ suite('Resolver Utils', () => { } throw new Error(`${command} is missing or is not executable`); }); - const actual = await resolveBasicEnv({ + const actual = await resolveEnv({ executablePath: path.join(TEST_LAYOUT_ROOT, 'conda1', 'python.exe'), kind: PythonEnvKind.Conda, }); @@ -268,7 +273,7 @@ suite('Resolver Utils', () => { } throw new Error(`${command} is missing or is not executable`); }); - const actual = await resolveBasicEnv({ + const actual = await resolveEnv({ executablePath: path.join(TEST_LAYOUT_ROOT, 'conda2', 'bin', 'python'), kind: PythonEnvKind.Conda, }); @@ -283,7 +288,7 @@ suite('Resolver Utils', () => { sinon.stub(externalDependencies, 'exec').callsFake(async (command: string) => { throw new Error(`${command} is missing or is not executable`); }); - const actual = await resolveBasicEnv({ + const actual = await resolveEnv({ executablePath: path.join(TEST_LAYOUT_ROOT, 'conda1', 'python.exe'), kind: PythonEnvKind.Conda, }); @@ -320,7 +325,7 @@ suite('Resolver Utils', () => { const info: PythonEnvInfo = { name, location, - kind, + kind: [kind], executable: { filename: interpreterPath, sysPrefix: '', @@ -346,7 +351,7 @@ suite('Resolver Utils', () => { 'win1', path.join(testVirtualHomeDir, '.venvs', 'win1'), ); - const actual = await resolveBasicEnv({ + const actual = await resolveEnv({ executablePath: path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), kind: PythonEnvKind.Venv, }); @@ -375,7 +380,7 @@ suite('Resolver Utils', () => { const info: PythonEnvInfo = { name, location, - kind, + kind: [kind], executable: { filename: interpreterPath, sysPrefix: '', @@ -396,7 +401,7 @@ suite('Resolver Utils', () => { test('resolveEnv', async () => { const executable = path.join(testLocation3, 'python3.8'); const expected = createExpectedEnvInfo(executable, PythonEnvKind.OtherGlobal, parseVersion('3.8')); - const actual = await resolveBasicEnv({ + const actual = await resolveEnv({ executablePath: executable, kind: PythonEnvKind.OtherGlobal, }); @@ -564,7 +569,7 @@ suite('Resolver Utils', () => { test('If data provided by registry is more informative than kind resolvers, use it to update environment (64bit)', async () => { const interpreterPath = path.join(regTestRoot, 'py39', 'python.exe'); - const actual = await resolveBasicEnv({ + const actual = await resolveEnv({ executablePath: interpreterPath, kind: PythonEnvKind.Unknown, source: [PythonEnvSource.WindowsRegistry], @@ -584,7 +589,7 @@ suite('Resolver Utils', () => { test('If data provided by registry is more informative than kind resolvers, use it to update environment (32bit)', async () => { const interpreterPath = path.join(regTestRoot, 'python38', 'python.exe'); - const actual = await resolveBasicEnv({ + const actual = await resolveEnv({ executablePath: interpreterPath, kind: PythonEnvKind.Unknown, source: [PythonEnvSource.WindowsRegistry, PythonEnvSource.PathEnvVar], @@ -607,7 +612,7 @@ suite('Resolver Utils', () => { throw new Error(`${command} is missing or is not executable`); }); const interpreterPath = path.join(regTestRoot, 'conda3', 'python.exe'); - const actual = await resolveBasicEnv({ + const actual = await resolveEnv({ executablePath: interpreterPath, kind: PythonEnvKind.Conda, source: [PythonEnvSource.WindowsRegistry],