From 0af181c6d8a7c1df4c1a2537c56e2b8b7badac4d Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 16 Aug 2022 01:18:05 -0700 Subject: [PATCH 01/60] Add a definitions file containing environments API --- types/proposed.environments.d.ts | 222 +++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 types/proposed.environments.d.ts diff --git a/types/proposed.environments.d.ts b/types/proposed.environments.d.ts new file mode 100644 index 000000000000..a57f310fd0d5 --- /dev/null +++ b/types/proposed.environments.d.ts @@ -0,0 +1,222 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/* 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'; + +// https://github.com/microsoft/vscode-python/wiki/Proposed-Environment-APIs + +export interface IProposedExtensionAPI { + environment: { + /** + * This event is triggered when the active environment changes. + */ + onDidActiveEnvironmentChanged: 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 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; + /** + * 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; + locator: { + /** + * 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()`. + * + * Environments lacking an interpreter are identified by environment folder paths, + * whereas other envs can be identified using executable path. + */ + getEnvironmentPaths(): Promise; + /** + * 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 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?: GetRefreshEnvironmentsOptions): 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 = KnownEnvSourceTypes | string; + +export enum KnownEnvSourceTypes { + Conda = 'Conda', + Pipenv = 'PipEnv', + Poetry = 'Poetry', + VirtualEnv = 'VirtualEnv', + Venv = 'Venv', + VirtualEnvWrapper = 'VirtualEnvWrapper', + Pyenv = 'Pyenv', +} + +export type EnvType = KnownEnvTypes | string; + +export enum KnownEnvTypes { + VirtualEnv = 'VirtualEnv', + Conda = 'Conda', + Unknown = 'Unknown', + Global = 'Global', +} + +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; +}; + +export interface EnvironmentDetails { + executable: { + path: string; + bitness?: Architecture; + sysPrefix: string; + // To be added later: + // run: { + // exec: Function; + // shellExec: Function; + // execObservable: Function; + // terminalExec: () => void; + // env?: { [key: string]: string | null | undefined }; + // }; + }; + environment?: { + type: EnvType; + name?: string; + path: string; + project?: string; // Any specific project environment is created for. + source: EnvSource[]; + }; + version: StandardVersionInfo & { + sysVersion?: string; + }; + implementation?: { + // `sys.implementation` + name: string; + version: StandardVersionInfo; + }; +} + +export interface EnvironmentDetailsOptions { + useCache: boolean; +} + +export interface GetRefreshEnvironmentsOptions { + /** + * 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; +}; + +export type Resource = Uri | undefined; + +/** + * Path to environment folder or path to interpreter that uniquely identifies an environment. + * Environments lacking an interpreter are identified by environment folder paths, + * whereas other envs can be identified using interpreter path. + */ +export type UniquePathType = string; + +export interface EnvPathType { + path: UniquePathType; + pathType: 'envFolderPath' | 'interpreterPath'; +} + +export interface EnvironmentsChangedParams { + path?: UniquePathType; + type: 'add' | 'remove' | 'update' | 'clear-all'; +} + +export interface ActiveEnvironmentChangedParams { + path: UniquePathType; + resource?: Uri; +} + +export interface RefreshEnvironmentsOptions { + clearCache?: boolean; +} From cf4060371339d2347750f618de3b05c62b9ba6d1 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 16 Aug 2022 09:00:08 -0700 Subject: [PATCH 02/60] Move definitions file --- .../proposed.environments.d.ts => src/client/proposedApiTypes.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename types/proposed.environments.d.ts => src/client/proposedApiTypes.ts (100%) diff --git a/types/proposed.environments.d.ts b/src/client/proposedApiTypes.ts similarity index 100% rename from types/proposed.environments.d.ts rename to src/client/proposedApiTypes.ts From 6e5976b148aa7521616efcf4d89345a016d869fd Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 16 Aug 2022 12:36:22 -0700 Subject: [PATCH 03/60] Update apis --- src/client/proposedApiTypes.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index a57f310fd0d5..a9e5a3ff684c 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -1,11 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -/* 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'; // https://github.com/microsoft/vscode-python/wiki/Proposed-Environment-APIs @@ -35,7 +30,7 @@ export interface IProposedExtensionAPI { * is partial data. */ getEnvironmentDetails( - path: string, + path: UniquePathType, options?: EnvironmentDetailsOptions, ): Promise; /** @@ -45,7 +40,7 @@ export interface IProposedExtensionAPI { * @param resource : [optional] Uri of a file ro workspace to scope to a particular workspace * folder. */ - setActiveEnvironment(path: string, resource?: Resource): Promise; + setActiveEnvironment(path: UniquePathType, resource?: Resource): Promise; locator: { /** * Returns paths to environments that uniquely identifies an environment found by the extension @@ -173,6 +168,9 @@ export interface EnvironmentDetails { } export interface EnvironmentDetailsOptions { + /** + * When true, cache is checked first for any data, returns even if there is partial data. + */ useCache: boolean; } From f885fd349b569f4437d010a1b6d328f36d053743 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 16 Aug 2022 14:22:39 -0700 Subject: [PATCH 04/60] Again --- src/client/proposedApiTypes.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index a9e5a3ff684c..86bd24a82b1a 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -207,14 +207,27 @@ export interface EnvPathType { export interface EnvironmentsChangedParams { path?: UniquePathType; + /** + * Types: + * * "add": New environment is added. + * * "remove": Existing environment in the list is removed. + * * "update": New information found about existing environment. + * * "clear-all": Remove all of the items in the list. (This is fired when refresh is triggered) + */ type: 'add' | 'remove' | 'update' | 'clear-all'; } export interface ActiveEnvironmentChangedParams { path: UniquePathType; + /** + * Uri of a file or workspace the environment changed for. + */ resource?: Uri; } export interface RefreshEnvironmentsOptions { + /** + * When `true`, this will clear the cache before environment refresh is triggered. + */ clearCache?: boolean; } From feafa6739899cab3150563528b88a01f02c54be1 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 17 Aug 2022 12:12:59 -0700 Subject: [PATCH 05/60] Update API based on api sync --- src/client/proposedApiTypes.ts | 124 +++++++++++++++++---------------- 1 file changed, 63 insertions(+), 61 deletions(-) diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index 86bd24a82b1a..cc49e53c4baa 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { Event, Uri } from 'vscode'; +import { Event, Uri, WorkspaceFolder } from 'vscode'; // https://github.com/microsoft/vscode-python/wiki/Proposed-Environment-APIs @@ -10,7 +10,7 @@ export interface IProposedExtensionAPI { /** * This event is triggered when the active environment changes. */ - onDidActiveEnvironmentChanged: Event; + 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 @@ -19,44 +19,41 @@ export interface IProposedExtensionAPI { * returns what ever is set for the workspace. * @param resource : Uri of a file or workspace */ - getActiveEnvironmentPath(resource?: Resource): Promise; + getActiveEnvironmentPath(resource?: Resource): Promise; /** - * Returns details for the given interpreter. Details such as absolute interpreter path, + * 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 path : Full path to environment folder or interpreter whose details you need. + * @param path : 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( - path: UniquePathType, + path: EnvironmentPath | UniquePathType, options?: EnvironmentDetailsOptions, ): 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 path : 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(path: UniquePathType, resource?: Resource): Promise; + setActiveEnvironment(path: EnvironmentPath | UniquePathType, resource?: Resource): Promise; locator: { /** * 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()`. - * - * Environments lacking an interpreter are identified by environment folder paths, - * whereas other envs can be identified using executable path. */ - getEnvironmentPaths(): Promise; + getEnvironmentPaths(): Promise; /** * 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; + 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 @@ -65,12 +62,12 @@ export interface IProposedExtensionAPI { * * clearCache : When true, this will clear the cache before environment refresh * is triggered. */ - refreshEnvironments(options?: RefreshEnvironmentsOptions): Promise; + refreshEnvironments(options?: RefreshEnvironmentsOptions): Promise; /** * Returns a promise for the ongoing refresh. Returns `undefined` if there are no active * refreshes going on. */ - getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise | undefined; + 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 @@ -87,26 +84,11 @@ export enum Architecture { x64 = 3, } -export type EnvSource = KnownEnvSourceTypes | string; - -export enum KnownEnvSourceTypes { - Conda = 'Conda', - Pipenv = 'PipEnv', - Poetry = 'Poetry', - VirtualEnv = 'VirtualEnv', - Venv = 'Venv', - VirtualEnvWrapper = 'VirtualEnvWrapper', - Pyenv = 'Pyenv', -} +export type EnvSource = KnownEnvSources | string; +export type KnownEnvSources = 'Conda' | 'Pipenv' | 'Poetry' | 'VirtualEnv' | 'Venv' | 'VirtualEnvWrapper' | 'Pyenv'; export type EnvType = KnownEnvTypes | string; - -export enum KnownEnvTypes { - VirtualEnv = 'VirtualEnv', - Conda = 'Conda', - Unknown = 'Unknown', - Global = 'Global', -} +export type KnownEnvTypes = 'VirtualEnv' | 'Conda' | 'Unknown'; export type BasicVersionInfo = { major: number; @@ -136,27 +118,35 @@ 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 }; +// }; + export interface EnvironmentDetails { executable: { path: string; bitness?: Architecture; sysPrefix: string; - // To be added later: - // run: { - // exec: Function; - // shellExec: Function; - // execObservable: Function; - // terminalExec: () => void; - // env?: { [key: string]: string | null | undefined }; - // }; - }; - environment?: { - type: EnvType; - name?: string; - path: string; - project?: string; // Any specific project environment is created for. - source: EnvSource[]; }; + environment: + | { + type: EnvType; + name?: string; + path: 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; }; @@ -174,7 +164,7 @@ export interface EnvironmentDetailsOptions { useCache: boolean; } -export interface GetRefreshEnvironmentsOptions { +export interface GetRefreshPromiseOptions { /** * Get refresh promise which resolves once the following stage has been reached for the list of known environments. */ @@ -191,38 +181,46 @@ export type ProgressNotificationEvent = { stage: ProgressReportStage; }; -export type Resource = Uri | undefined; +/** + * Uri of a file inside a workspace or workspace folder itself. + */ +export type Resource = Uri | WorkspaceFolder; /** - * Path to environment folder or path to interpreter that uniquely identifies an environment. - * Environments lacking an interpreter are identified by environment folder paths, - * whereas other envs can be identified using interpreter path. + * 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 EnvPathType { - path: UniquePathType; - pathType: 'envFolderPath' | 'interpreterPath'; +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 interface EnvironmentsChangedParams { - path?: UniquePathType; + pathID?: UniquePathType; /** * Types: * * "add": New environment is added. * * "remove": Existing environment in the list is removed. * * "update": New information found about existing environment. - * * "clear-all": Remove all of the items in the list. (This is fired when refresh is triggered) + * * "clear-all": Remove all of the items in the list. (This is fired when a hard refresh is triggered) */ type: 'add' | 'remove' | 'update' | 'clear-all'; } export interface ActiveEnvironmentChangedParams { - path: UniquePathType; + pathID: UniquePathType; /** - * Uri of a file or workspace the environment changed for. + * Uri of a file inside a workspace or workspace folder the environment changed for. */ - resource?: Uri; + resource?: Resource; } export interface RefreshEnvironmentsOptions { @@ -230,4 +228,8 @@ 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; } From 53a4b89d27c9471e9c5c54095842f1bcedee871a Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 31 Aug 2022 10:43:43 -0700 Subject: [PATCH 06/60] Turn getEnvironmentPaths API to sync --- src/client/proposedApiTypes.ts | 40 ++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index cc49e53c4baa..55be64c91681 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -44,11 +44,11 @@ export interface IProposedExtensionAPI { locator: { /** * 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 + * 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(): Promise; + 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. @@ -203,17 +203,29 @@ export interface EnvironmentPath { executablePath: string | undefined; } -export interface EnvironmentsChangedParams { - pathID?: UniquePathType; - /** - * Types: - * * "add": New environment is added. - * * "remove": Existing environment in the list is removed. - * * "update": New information found about existing environment. - * * "clear-all": Remove all of the items in the list. (This is fired when a hard refresh is triggered) - */ - type: 'add' | 'remove' | 'update' | 'clear-all'; -} +export type EnvironmentsChangedParams = + | { + pathID: UniquePathType; + /** + * * "add": New environment is added. + * * "remove": Existing environment in the list is removed. + * * "update": New information found about existing environment. + */ + type: 'add' | 'remove' | 'update'; + } + | { + /** + * * "clear-all": Remove all of the items in the list. (This is fired when a hard refresh is triggered) + */ + type: 'clear-all'; + } + | { + location: string; + /** + * * "created": New environment is created in some location. + */ + type: 'created'; + }; export interface ActiveEnvironmentChangedParams { pathID: UniquePathType; From bca26b175bfdb9cdc5eea5831156c46c131749cb Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 31 Aug 2022 14:44:10 -0700 Subject: [PATCH 07/60] Add to types --- src/client/proposedApiTypes.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index 55be64c91681..133ea1ac31fc 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -24,23 +24,23 @@ export interface IProposedExtensionAPI { * 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 path : Full path to environment folder or python executable whose details you need. + * @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( - path: EnvironmentPath | UniquePathType, + 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 path : Full path to environment folder or python executable to set. + * @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(path: EnvironmentPath | UniquePathType, resource?: Resource): Promise; + setActiveEnvironment(pathID: UniquePathType | EnvironmentPath, resource?: Resource): Promise; locator: { /** * Returns paths to environments that uniquely identifies an environment found by the extension @@ -137,7 +137,7 @@ export interface EnvironmentDetails { | { type: EnvType; name?: string; - path: 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. @@ -205,7 +205,7 @@ export interface EnvironmentPath { export type EnvironmentsChangedParams = | { - pathID: UniquePathType; + path: EnvironmentPath; /** * * "add": New environment is added. * * "remove": Existing environment in the list is removed. @@ -220,6 +220,9 @@ export type EnvironmentsChangedParams = type: 'clear-all'; } | { + /** + * The location at which the environment got created. + */ location: string; /** * * "created": New environment is created in some location. From 1fa9a149d868f720bf4c1f5b91629aee60cd71de Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 31 Aug 2022 15:20:58 -0700 Subject: [PATCH 08/60] Emit the workspace folder in event --- src/client/proposedApiTypes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index 133ea1ac31fc..1a89dc60c528 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -233,9 +233,9 @@ export type EnvironmentsChangedParams = export interface ActiveEnvironmentChangedParams { pathID: UniquePathType; /** - * Uri of a file inside a workspace or workspace folder the environment changed for. + * Workspace folder the environment changed for. */ - resource?: Resource; + resource?: WorkspaceFolder; } export interface RefreshEnvironmentsOptions { From 5a45c9c69e8edabe9abd1b3599c95572f9a91cbd Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 31 Aug 2022 15:26:00 -0700 Subject: [PATCH 09/60] Merge EnvironmentPath into event type --- src/client/proposedApiTypes.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index 1a89dc60c528..e65f9ca5bbca 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -204,15 +204,14 @@ export interface EnvironmentPath { } export type EnvironmentsChangedParams = - | { - path: EnvironmentPath; + | ({ /** * * "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) @@ -235,7 +234,7 @@ export interface ActiveEnvironmentChangedParams { /** * Workspace folder the environment changed for. */ - resource?: WorkspaceFolder; + resource: WorkspaceFolder | undefined; } export interface RefreshEnvironmentsOptions { From b99c71d1d80ab9c8677bd7a08c48830072c62843 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 6 Sep 2022 17:42:31 -0700 Subject: [PATCH 10/60] Jupyter API review --- src/client/proposedApiTypes.ts | 99 +++++++++------------------------- 1 file changed, 24 insertions(+), 75 deletions(-) diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index e65f9ca5bbca..5104c20dea22 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -21,18 +21,16 @@ export interface IProposedExtensionAPI { */ 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. + * Returns details for the given environment path, or `undefined` if the env is invalid. * @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; + getEnvironmentDetails(pathID: UniquePathType | EnvironmentPath): Promise; + /** + * Returns current in-memory details for the given environment path if any, even if there's partial data. + * Only returns if the final type and name of an environment is known. + * @param pathID : Full path to environment folder or python executable whose details you need. + */ + getEnvironmentDetailsSync(pathID: UniquePathType | EnvironmentPath): EnvironmentDetails | undefined; /** * Sets the active environment path for the python extension for the resource. Configuration target * will always be the workspace folder. @@ -44,9 +42,8 @@ export interface IProposedExtensionAPI { 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()`. + * at the time of calling. It returns the values currently in memory, which carries what is + * known so far. To get complete list `await` on promise returned by `getRefreshPromise()`. */ getEnvironmentPaths(): EnvironmentPath[] | undefined; /** @@ -55,23 +52,18 @@ export interface IProposedExtensionAPI { */ 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. + * This API will trigger environment discovery if not already triggered for the session. If + * there is a refresh already going on then it returns the promise for that refresh. */ - refreshEnvironments(options?: RefreshEnvironmentsOptions): Promise; + refreshEnvironments(): Promise; /** * Returns a promise for the ongoing refresh. Returns `undefined` if there are no active * refreshes going on. */ - getRefreshPromise(options?: GetRefreshPromiseOptions): Promise | undefined; + getRefreshPromise(): 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. + * stage. */ readonly onRefreshProgress: Event; }; @@ -128,6 +120,7 @@ export type StandardVersionInfo = BasicVersionInfo & { // }; export interface EnvironmentDetails { + pathID: UniquePathType; executable: { path: string; bitness?: Architecture; @@ -151,29 +144,13 @@ export interface EnvironmentDetails { 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', } @@ -203,31 +180,14 @@ export interface EnvironmentPath { 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 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; export interface ActiveEnvironmentChangedParams { pathID: UniquePathType; @@ -236,14 +196,3 @@ export interface ActiveEnvironmentChangedParams { */ 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; -} From 51a1c55302d4109adfbe81450cf8bbc5379611e7 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 8 Sep 2022 15:19:28 -0700 Subject: [PATCH 11/60] Jupyter review Day 2 --- src/client/proposedApiTypes.ts | 44 +++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index 5104c20dea22..171a4ce25730 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -30,7 +30,7 @@ export interface IProposedExtensionAPI { * Only returns if the final type and name of an environment is known. * @param pathID : Full path to environment folder or python executable whose details you need. */ - getEnvironmentDetailsSync(pathID: UniquePathType | EnvironmentPath): EnvironmentDetails | undefined; + getEnvironmentDetailsSync(pathID: UniquePathType | EnvironmentPath): PartialEnvironmentDetails | undefined; /** * Sets the active environment path for the python extension for the resource. Configuration target * will always be the workspace folder. @@ -45,17 +45,12 @@ export interface IProposedExtensionAPI { * at the time of calling. It returns the values currently in memory, which carries what is * known so far. To get complete list `await` on promise returned by `getRefreshPromise()`. */ - getEnvironmentPaths(): EnvironmentPath[] | undefined; + getEnvironments(): UniquePathType[] | 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 trigger environment discovery if not already triggered for the session. If - * there is a refresh already going on then it returns the promise for that refresh. - */ - refreshEnvironments(): Promise; + onDidChangeEnvironments: Event; /** * Returns a promise for the ongoing refresh. Returns `undefined` if there are no active * refreshes going on. @@ -149,6 +144,36 @@ export interface EnvironmentDetails { }; } +export interface PartialEnvironmentDetails { + pathID: UniquePathType; + executable: { + path: string; + bitness?: Architecture; + sysPrefix?: string; + }; + environment: + | { + type: EnvType; + name: string | undefined; + 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: Partial & { + sysVersion?: string; + }; + implementation?: { + name: string; + version: Partial; + }; +} + export enum ProgressReportStage { discoveryStarted = 'discoveryStarted', discoveryFinished = 'discoveryFinished', @@ -181,13 +206,14 @@ export interface EnvironmentPath { } export type EnvironmentsChangedParams = { + pathID: UniquePathType; /** * * "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; +}; export interface ActiveEnvironmentChangedParams { pathID: UniquePathType; From 5bbb7e540394fc767f6040662dfedc7384f97058 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 9 Sep 2022 17:03:23 -0700 Subject: [PATCH 12/60] Jupyter review 3 --- src/client/proposedApiTypes.ts | 164 +++++++++++++++++---------------- 1 file changed, 85 insertions(+), 79 deletions(-) diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index 171a4ce25730..3deeeb70b1c9 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -6,63 +6,73 @@ 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 environment path, or `undefined` if the env is invalid. - * @param pathID : Full path to environment folder or python executable whose details you need. - */ - getEnvironmentDetails(pathID: UniquePathType | EnvironmentPath): Promise; - /** - * Returns current in-memory details for the given environment path if any, even if there's partial data. - * Only returns if the final type and name of an environment is known. - * @param pathID : Full path to environment folder or python executable whose details you need. - */ - getEnvironmentDetailsSync(pathID: UniquePathType | EnvironmentPath): PartialEnvironmentDetails | undefined; - /** - * 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, which carries what is - * known so far. To get complete list `await` on promise returned by `getRefreshPromise()`. - */ - getEnvironments(): UniquePathType[] | 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; - /** - * Returns a promise for the ongoing refresh. Returns `undefined` if there are no active - * refreshes going on. - */ - getRefreshPromise(): Promise | undefined; - /** - * Tracks discovery progress for current list of known environments, i.e when it starts, finishes or any other relevant - * stage. - */ - readonly onRefreshProgress: Event; - }; - }; + environment: IEnvironmentAPI; +} + +interface IEnvironmentAPI { + /** + * This event is triggered when the active environment changes. + */ + onDidChangeActiveEnvironment: Event; + /** + * Returns the environment selected by the user or as in the settings. 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 + */ + getActiveEnvironment(resource?: Resource): Promise; + /** + * Returns details for the given environment, or `undefined` if the env is invalid. + * @param environment : Environment whose details you need. Can also pass the full path to environment folder or python executable for the environment. + */ + resolveEnvironment(environment: Environment | UniquePathType): Promise; + /** + * Sets the active environment path for the python extension for the resource. Configuration target + * will always be the workspace folder. + * @param environment : Full path to environment folder or python executable for the environment. Can also pass the environment itself. + * @param resource : [optional] Uri of a file ro workspace to scope to a particular workspace + * folder. + */ + setActiveEnvironment(environment: Environment | UniquePathType, resource?: Resource): Promise; + /** + * Carries the API necessary for locating environments. + */ + locator: IEnvironmentLocatorAPI; +} + +interface IEnvironmentLocatorAPI { + /** + * Carries environments found by the extension at the time of fetching the property. To get complete list + * `await` on promise returned by `getRefreshPromise()`. + * + * Only returns an environment if the final type, name and environment path is known. + */ + environments: Environment[] | 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; + /** + * Returns a promise for the ongoing refresh. Returns `undefined` if there are no active + * refreshes going on. + */ + getRefreshPromise(): Promise | undefined; + /** + * Tracks discovery progress for current list of known environments, i.e when it starts, finishes or any other relevant + * stage. + */ + readonly onRefreshProgress: Event; + /** + * This API will re-trigger environment discovery. If there is a refresh already going on + * then it returns the promise for that refresh. + * + * Note this can be expensive so it's best to only use it if user manually triggers it. For + * internal automatic triggers consider using {@link RefreshOptions.bestEffortRefresh}. + * @param options Additonal options for refresh. + */ + refreshEnvironment(options?: RefreshOptions): Promise; } export enum Architecture { @@ -105,16 +115,7 @@ 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 }; -// }; - -export interface EnvironmentDetails { +export interface ResolvedEnvironment { pathID: UniquePathType; executable: { path: string; @@ -124,14 +125,14 @@ export interface EnvironmentDetails { environment: | { type: EnvType; - name?: string; + name: string | undefined; 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; + workspaceFolder: Uri | undefined; source: EnvSource[]; } | undefined; @@ -144,10 +145,10 @@ export interface EnvironmentDetails { }; } -export interface PartialEnvironmentDetails { +export interface Environment { pathID: UniquePathType; executable: { - path: string; + path: string | undefined; bitness?: Architecture; sysPrefix?: string; }; @@ -161,7 +162,7 @@ export interface PartialEnvironmentDetails { * 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; + workspaceFolder: Uri | undefined; source: EnvSource[]; } | undefined; @@ -174,13 +175,8 @@ export interface PartialEnvironmentDetails { }; } -export enum ProgressReportStage { - discoveryStarted = 'discoveryStarted', - discoveryFinished = 'discoveryFinished', -} - export type ProgressNotificationEvent = { - stage: ProgressReportStage; + stage: 'started' | 'finished'; }; /** @@ -206,7 +202,7 @@ export interface EnvironmentPath { } export type EnvironmentsChangedParams = { - pathID: UniquePathType; + env: Environment; /** * * "add": New environment is added. * * "remove": Existing environment in the list is removed. @@ -222,3 +218,13 @@ export interface ActiveEnvironmentChangedParams { */ resource: WorkspaceFolder | undefined; } + +export type RefreshOptions = { + /** + * Useful when triggering a refresh automatically based on internal code. This currently: + * * Only starts a refresh if it hasn't already been triggered for this session. + * * This option can later also be used to support refresh for only new environments, where + * possible, instead of triggering a full blown refresh. + */ + bestEffortRefresh?: boolean; +}; From 0701b607535e2fec81e91c9f24e447e10f6a0050 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Sun, 11 Sep 2022 23:41:30 -0700 Subject: [PATCH 13/60] Derive Environment type using ResolvedEnvironment --- src/client/proposedApiTypes.ts | 89 +++++++++++++--------------------- 1 file changed, 33 insertions(+), 56 deletions(-) diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index 3deeeb70b1c9..5e234b65df5a 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -87,12 +87,6 @@ export type KnownEnvSources = 'Conda' | 'Pipenv' | 'Poetry' | 'VirtualEnv' | 'Ve 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. */ @@ -111,68 +105,49 @@ export type PythonVersionRelease = { serial: number; }; -export type StandardVersionInfo = BasicVersionInfo & { +export type StandardVersionInfo = { + major: number; + minor: number; + micro: number; release?: PythonVersionRelease; }; +type ResolvedEnvironmentInfo = { + type: EnvType; + name: string | undefined; + 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 | undefined; + source: EnvSource[]; +}; export interface ResolvedEnvironment { pathID: UniquePathType; executable: { path: string; - bitness?: Architecture; + bitness: Architecture; sysPrefix: string; }; - environment: - | { - type: EnvType; - name: string | undefined; - 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 | undefined; - source: EnvSource[]; - } - | undefined; + environment: ResolvedEnvironmentInfo | undefined; version: StandardVersionInfo & { - sysVersion?: string; - }; - implementation?: { - name: string; - version: StandardVersionInfo; + sysVersion: string; }; } +type MakeOptional = Omit & Partial>; +type EnvironmentInfo = MakeOptional; +type ExecutableInfo = MakeOptional & + MakeOptional; +type PythonVersionInfo = Partial; + export interface Environment { pathID: UniquePathType; - executable: { - path: string | undefined; - bitness?: Architecture; - sysPrefix?: string; - }; - environment: - | { - type: EnvType; - name: string | undefined; - 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 | undefined; - source: EnvSource[]; - } - | undefined; - version: Partial & { - sysVersion?: string; - }; - implementation?: { - name: string; - version: Partial; - }; + executable: ExecutableInfo; + environment: EnvironmentInfo | undefined; + version: PythonVersionInfo; } export type ProgressNotificationEvent = { @@ -221,9 +196,11 @@ export interface ActiveEnvironmentChangedParams { export type RefreshOptions = { /** - * Useful when triggering a refresh automatically based on internal code. This currently: - * * Only starts a refresh if it hasn't already been triggered for this session. - * * This option can later also be used to support refresh for only new environments, where + * Optimized refresh which tries its best to keep environments upto date. Useful when + * triggering a refresh automatically based on internal code. + * + * This currently only starts a refresh if it hasn't already been triggered for this session. + * It can later also be amended to support refresh for only new environments, where * possible, instead of triggering a full blown refresh. */ bestEffortRefresh?: boolean; From bbba12aaf063b5ffaf86470f423b22ecb9ab3370 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 12 Sep 2022 00:12:49 -0700 Subject: [PATCH 14/60] Derive ResolvedEnvironment using Environment type --- src/client/proposedApiTypes.ts | 56 ++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index 5e234b65df5a..5db3eb044b37 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -48,7 +48,7 @@ interface IEnvironmentLocatorAPI { * * Only returns an environment if the final type, name and environment path is known. */ - environments: Environment[] | undefined; + environments: readonly Environment[] | 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. @@ -112,38 +112,42 @@ export type StandardVersionInfo = { release?: PythonVersionRelease; }; -type ResolvedEnvironmentInfo = { - type: EnvType; - name: string | undefined; - 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 | undefined; - source: EnvSource[]; -}; -export interface ResolvedEnvironment { +export interface Environment { pathID: UniquePathType; executable: { - path: string; - bitness: Architecture; - sysPrefix: string; + path: string | undefined; + bitness?: Architecture; + sysPrefix?: string; }; - environment: ResolvedEnvironmentInfo | undefined; - version: StandardVersionInfo & { - sysVersion: string; + environment: + | { + type: EnvType; + name: string | undefined; + 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 | undefined; + source: EnvSource[]; + } + | undefined; + version: Partial & { + sysVersion?: string; }; } -type MakeOptional = Omit & Partial>; -type EnvironmentInfo = MakeOptional; -type ExecutableInfo = MakeOptional & - MakeOptional; -type PythonVersionInfo = Partial; +type MakeRequired = Omit & Required>; +type ExecutableInfo = MakeRequired & + MakeRequired; +type EnvironmentInfo = MakeRequired, 'environment'>['environment']; +export type PythonVersionInfo = Required; -export interface Environment { +/** + * Derived form of {@link Environment} with complete information. + */ +export interface ResolvedEnvironment { pathID: UniquePathType; executable: ExecutableInfo; environment: EnvironmentInfo | undefined; From 5e3f510e593c6238c0dcb823a9509268dcdc49b6 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 12 Sep 2022 10:05:32 -0700 Subject: [PATCH 15/60] Use non-nullable types --- src/client/proposedApiTypes.ts | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index 5db3eb044b37..d3ecc1a54971 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -106,18 +106,18 @@ export type PythonVersionRelease = { }; export type StandardVersionInfo = { - major: number; - minor: number; - micro: number; - release?: PythonVersionRelease; + major: number | undefined; + minor: number | undefined; + micro: number | undefined; + release: PythonVersionRelease | undefined; }; export interface Environment { pathID: UniquePathType; executable: { path: string | undefined; - bitness?: Architecture; - sysPrefix?: string; + bitness: Architecture | undefined; + sysPrefix: string | undefined; }; environment: | { @@ -133,16 +133,19 @@ export interface Environment { source: EnvSource[]; } | undefined; - version: Partial & { - sysVersion?: string; + version: StandardVersionInfo & { + sysVersion: string | undefined; }; } -type MakeRequired = Omit & Required>; -type ExecutableInfo = MakeRequired & - MakeRequired; -type EnvironmentInfo = MakeRequired, 'environment'>['environment']; -export type PythonVersionInfo = Required; +type MakeNonNullable = Omit & NonNullable>; +type MakeAllPropertiesNonNullable = { + [P in keyof T]: NonNullable; +}; +type ExecutableInfo = MakeNonNullable & + MakeNonNullable; +type EnvironmentInfo = NonNullable['environment']>; +export type PythonVersionInfo = MakeAllPropertiesNonNullable; /** * Derived form of {@link Environment} with complete information. From f5716a64ef4c16c8df536ba5ebda941fa0af51ce Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 12 Sep 2022 10:33:25 -0700 Subject: [PATCH 16/60] Rename getRefreshPromise to waitOnRefresh --- src/client/proposedApiTypes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index d3ecc1a54971..9ad73df5e23b 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -44,7 +44,7 @@ interface IEnvironmentAPI { interface IEnvironmentLocatorAPI { /** * Carries environments found by the extension at the time of fetching the property. To get complete list - * `await` on promise returned by `getRefreshPromise()`. + * `await` on promise returned by {@link waitOnRefresh()}. * * Only returns an environment if the final type, name and environment path is known. */ @@ -58,7 +58,7 @@ interface IEnvironmentLocatorAPI { * Returns a promise for the ongoing refresh. Returns `undefined` if there are no active * refreshes going on. */ - getRefreshPromise(): Promise | undefined; + waitOnRefresh(): Promise | undefined; /** * Tracks discovery progress for current list of known environments, i.e when it starts, finishes or any other relevant * stage. From b824bf92f981948ab66195f4752728a7b3a6dde8 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 12 Sep 2022 11:13:41 -0700 Subject: [PATCH 17/60] Update types based on guidelines --- src/client/proposedApiTypes.ts | 101 +++++++++++++++------------------ 1 file changed, 45 insertions(+), 56 deletions(-) diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index 9ad73df5e23b..460491ab3ab6 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { Event, Uri, WorkspaceFolder } from 'vscode'; +import { CancellationToken, Event, Uri, WorkspaceFolder } from 'vscode'; // https://github.com/microsoft/vscode-python/wiki/Proposed-Environment-APIs @@ -65,53 +65,17 @@ interface IEnvironmentLocatorAPI { */ readonly onRefreshProgress: Event; /** - * This API will re-trigger environment discovery. If there is a refresh already going on - * then it returns the promise for that refresh. + * This API will re-trigger environment discovery. If there is a refresh already going on then it + * returns the promise for that refresh. * * Note this can be expensive so it's best to only use it if user manually triggers it. For * internal automatic triggers consider using {@link RefreshOptions.bestEffortRefresh}. * @param options Additonal options for refresh. + * @param token A cancellation token that indicates a refresh is no longer needed. */ - refreshEnvironment(options?: RefreshOptions): Promise; + refreshEnvironment(options: RefreshOptions, token?: CancellationToken): Promise; } -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'; - -/** - * 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 = { - major: number | undefined; - minor: number | undefined; - micro: number | undefined; - release: PythonVersionRelease | undefined; -}; - export interface Environment { pathID: UniquePathType; executable: { @@ -126,8 +90,6 @@ export interface Environment { 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 | undefined; source: EnvSource[]; @@ -173,16 +135,6 @@ export type Resource = Uri | WorkspaceFolder; */ 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 = { env: Environment; /** @@ -203,12 +155,49 @@ export interface ActiveEnvironmentChangedParams { export type RefreshOptions = { /** - * Optimized refresh which tries its best to keep environments upto date. Useful when + * Faster refresh which tries its best to keep environments upto date. Useful when * triggering a refresh automatically based on internal code. * * This currently only starts a refresh if it hasn't already been triggered for this session. - * It can later also be amended to support refresh for only new environments, where + * It can later also be amended/updated to support refresh for only new environments, where * possible, instead of triggering a full blown refresh. */ - bestEffortRefresh?: boolean; + bestEffortRefresh: boolean | undefined; +}; + +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 enum Architecture { + Unknown = 1, + x86 = 2, + x64 = 3, +} + +/** + * 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 = { + major: number | undefined; + minor: number | undefined; + micro: number | undefined; + release: PythonVersionRelease | undefined; }; From 6ebfa8f016fe8343c1c1499affcc76643161cc50 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 12 Sep 2022 16:23:46 -0700 Subject: [PATCH 18/60] Remove `bestEffortRefresh` in favor of a new option --- src/client/proposedApiTypes.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index 460491ab3ab6..a1f4d9949b01 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -69,7 +69,7 @@ interface IEnvironmentLocatorAPI { * returns the promise for that refresh. * * Note this can be expensive so it's best to only use it if user manually triggers it. For - * internal automatic triggers consider using {@link RefreshOptions.bestEffortRefresh}. + * internal automatic triggers consider using {@link RefreshOptions.ifNotRefreshedAlready}. * @param options Additonal options for refresh. * @param token A cancellation token that indicates a refresh is no longer needed. */ @@ -155,14 +155,12 @@ export interface ActiveEnvironmentChangedParams { export type RefreshOptions = { /** - * Faster refresh which tries its best to keep environments upto date. Useful when - * triggering a refresh automatically based on internal code. + * Only trigger a refresh if not triggered already for this session. Useful for making sure env + * list is up-to-date when extension starts up. * - * This currently only starts a refresh if it hasn't already been triggered for this session. - * It can later also be amended/updated to support refresh for only new environments, where - * possible, instead of triggering a full blown refresh. + * After that users can use extension specific UI to refresh environments when needed. */ - bestEffortRefresh: boolean | undefined; + ifNotRefreshedAlready: boolean | undefined; }; export type EnvSource = KnownEnvSources | string; From 388699f8288f17987a0adc8326966c7cec8e6d5e Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 14 Sep 2022 17:07:34 -0700 Subject: [PATCH 19/60] Add implementation --- src/client/interpreter/interpreterService.ts | 6 +- src/client/proposedApi.ts | 228 ++++++++++++------ .../locators/composite/envsCollectionCache.ts | 12 - .../envsCollectionService.unit.test.ts | 26 -- 4 files changed, 154 insertions(+), 118 deletions(-) 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..a0a878c1070a 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -2,49 +2,66 @@ // Licensed under the MIT License. import { ConfigurationTarget, EventEmitter } from 'vscode'; +import { IDisposableRegistry, IInterpreterPathService } from './common/types'; +import { IInterpreterService } from './interpreter/contracts'; +import { IServiceContainer } from './ioc/types'; import { ActiveEnvironmentChangedParams, - EnvironmentDetails, - EnvironmentDetailsOptions, + Environment, 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 { PythonEnvInfo } from './pythonEnvironments/base/info'; + ResolvedEnvironment, + ProgressNotificationEvent, + UniquePathType, + PythonVersionInfo, + RefreshOptions, + Resource, +} from './proposedApiTypes'; +import { PythonEnvInfo, PythonEnvKind, virtualEnvKinds } from './pythonEnvironments/base/info'; import { getEnvPath } from './pythonEnvironments/base/info/env'; -import { GetRefreshEnvironmentsOptions, IDiscoveryAPI } from './pythonEnvironments/base/locator'; - -const onDidInterpretersChangedEvent = new EventEmitter(); -export function reportInterpretersChanged(e: EnvironmentsChangedParams[]): void { - onDidInterpretersChangedEvent.fire(e); -} +import { IDiscoveryAPI, ProgressReportStage } from './pythonEnvironments/base/locator'; const onDidActiveInterpreterChangedEvent = new EventEmitter(); export function reportActiveInterpreterChanged(e: ActiveEnvironmentChangedParams): void { onDidActiveInterpreterChangedEvent.fire(e); } +const onProgress = new EventEmitter(); +const onEnvironmentsChanged = new EventEmitter(); +const environmentsReference = new Map(); + +class EnvironmentReference implements Environment { + readonly pathID: string; + + constructor(private internal: Environment) { + this.pathID = internal.pathID; + } + + get executable() { + return this.internal.executable; + } + + get environment() { + return this.internal.environment; + } + + get version() { + return this.internal.version; + } -function getVersionString(env: PythonEnvInfo): string[] { - const ver = [`${env.version.major}`, `${env.version.minor}`, `${env.version.micro}`]; - if (env.version.release) { - ver.push(`${env.version.release}`); - if (env.version.sysVersion) { - ver.push(`${env.version.release}`); - } + updateEnv(newInternal: Environment) { + this.internal = newInternal; } - return ver; } -/** - * Returns whether the path provided matches the environment. - * @param path Path to environment folder or path to interpreter that uniquely identifies an environment. - * @param env Environment to match with. - */ -function isEnvSame(path: string, env: PythonEnvInfo) { - return arePathsSame(path, env.location) || arePathsSame(path, env.executable.filename); +function getEnvReference(e: Environment) { + let envClass = environmentsReference.get(e.pathID); + if (!envClass) { + envClass = new EnvironmentReference(e); + } else { + envClass.updateEnv(e); + } + environmentsReference.set(e.pathID, envClass); + return envClass; } export function buildProposedApi( @@ -53,66 +70,123 @@ export function buildProposedApi( ): IProposedExtensionAPI { const interpreterPathService = serviceContainer.get(IInterpreterPathService); const interpreterService = serviceContainer.get(IInterpreterService); - + const disposables = serviceContainer.get(IDisposableRegistry); + disposables.push( + discoveryApi.onProgress((e) => { + if (e.stage === ProgressReportStage.discoveryStarted) { + onProgress.fire({ stage: 'started' }); + } + if (e.stage === ProgressReportStage.discoveryFinished) { + onProgress.fire({ stage: 'finished' }); + } + }), + discoveryApi.onChanged((e) => { + if (e.old) { + if (e.new) { + onEnvironmentsChanged.fire({ type: 'update', env: convertEnvInfoAndGetReference(e.new) }); + } else { + onEnvironmentsChanged.fire({ type: 'remove', env: convertEnvInfoAndGetReference(e.old) }); + } + } else if (e.new) { + onEnvironmentsChanged.fire({ type: 'add', env: convertEnvInfoAndGetReference(e.new) }); + } + }), + onProgress, + onEnvironmentsChanged, + ); const proposed: IProposedExtensionAPI = { environment: { - async getExecutionDetails(resource?: Resource) { - const env = await interpreterService.getActiveInterpreter(resource); - return env ? { execCommand: [env.path] } : { execCommand: undefined }; - }, - async getActiveEnvironmentPath(resource?: Resource) { + async getActiveEnvironment(resource?: Resource) { + resource = resource && 'uri' in resource ? resource.uri : resource; const env = await interpreterService.getActiveInterpreter(resource); if (!env) { return undefined; } - return getEnvPath(env.path, env.envPath); + return resolveEnvironment(getEnvPath(env.path, env.envPath).path, discoveryApi); }, - async getEnvironmentDetails( - path: string, - options?: EnvironmentDetailsOptions, - ): Promise { - let env: PythonEnvInfo | undefined; - if (options?.useCache) { - env = discoveryApi.getEnvs().find((v) => isEnvSame(path, v)); - } - if (!env) { - env = await discoveryApi.resolveEnv(path); - if (!env) { - return undefined; - } - } - return { - interpreterPath: env.executable.filename, - envFolderPath: env.location.length ? env.location : undefined, - version: getVersionString(env), - environmentType: [env.kind], - metadata: { - sysPrefix: env.executable.sysPrefix, - bitness: env.arch, - project: env.searchLocation, - }, - }; + resolveEnvironment: (env: string | Environment) => { + const path = typeof env !== 'string' ? env.pathID : env; + return resolveEnvironment(path, discoveryApi); }, - getEnvironmentPaths() { - const paths = discoveryApi.getEnvs().map((e) => getEnvPath(e.executable.filename, e.location)); - return Promise.resolve(paths); + locator: { + get environments(): Environment[] { + return discoveryApi.getEnvs().map((e) => convertEnvInfoAndGetReference(e)); + }, + get onRefreshProgress() { + return onProgress.event; + }, + waitOnRefresh(): Promise | undefined { + return discoveryApi.getRefreshPromise(); + }, + async refreshEnvironment(options: RefreshOptions) { + await discoveryApi.triggerRefresh(undefined, { + ifNotTriggerredAlready: options.ifNotRefreshedAlready, + }); + }, + get onDidChangeEnvironments() { + return onEnvironmentsChanged.event; + }, }, - setActiveEnvironment(path: string, resource?: Resource): Promise { + setActiveEnvironment(env: string | Environment, resource?: Resource): Promise { + const path = typeof env !== 'string' ? env.pathID : env; + resource = resource && 'uri' in resource ? resource.uri : resource; return interpreterPathService.update(resource, ConfigurationTarget.WorkspaceFolder, path); }, - async refreshEnvironment() { - await discoveryApi.triggerRefresh(); - const paths = discoveryApi.getEnvs().map((e) => getEnvPath(e.executable.filename, e.location)); - return Promise.resolve(paths); - }, - getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise | undefined { - return discoveryApi.getRefreshPromise(options); - }, - onDidChangeExecutionDetails: interpreterService.onDidChangeInterpreterConfiguration, - onDidEnvironmentsChanged: onDidInterpretersChangedEvent.event, - onDidActiveEnvironmentChanged: onDidActiveInterpreterChangedEvent.event, - onRefreshProgress: discoveryApi.onProgress, + onDidChangeActiveEnvironment: onDidActiveInterpreterChangedEvent.event, }, }; return proposed; } + +async function resolveEnvironment(path: string, discoveryApi: IDiscoveryAPI): Promise { + const env = await discoveryApi.resolveEnv(path); + if (!env) { + return undefined; + } + return getEnvReference(convertCompleteEnvInfo(env)) as ResolvedEnvironment; +} + +function convertCompleteEnvInfo(env: PythonEnvInfo): ResolvedEnvironment { + const version = { ...env.version, sysVersion: env.version.sysVersion }; + return { + pathID: getEnvPath(env.executable.filename, env.location).path, + executable: { + path: env.executable.filename, + bitness: env.arch, + sysPrefix: env.executable.sysPrefix, + }, + environment: virtualEnvKinds.includes(env.kind) + ? { + type: env.kind === PythonEnvKind.Conda ? 'Conda' : 'VirtualEnv', + name: env.name, + folderPath: env.location, + workspaceFolder: env.searchLocation, + source: [env.kind], + } + : undefined, + version: version as PythonVersionInfo, + }; +} + +function convertEnvInfoAndGetReference(env: PythonEnvInfo): Environment { + const convertedEnv = convertCompleteEnvInfo(env) as Environment; + if (convertedEnv.executable.sysPrefix === '') { + convertedEnv.executable.sysPrefix = undefined; + } + if (convertedEnv.executable.path === 'python') { + convertedEnv.executable.path = undefined; + } + if (convertedEnv.environment?.name === '') { + convertedEnv.environment.name = undefined; + } + if (convertedEnv.version.major === -1) { + convertedEnv.version.major = undefined; + } + if (convertedEnv.version.micro === -1) { + convertedEnv.version.micro = undefined; + } + if (convertedEnv.version.minor === -1) { + convertedEnv.version.minor = undefined; + } + return getEnvReference(convertedEnv); +} diff --git a/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts b/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts index 14663e2d117d..a8820a0f82b8 100644 --- a/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts +++ b/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts @@ -4,7 +4,6 @@ import { Event } from 'vscode'; import { isTestExecution } from '../../../../common/constants'; import { traceInfo } from '../../../../logging'; -import { reportInterpretersChanged } from '../../../../proposedApi'; import { arePathsSame, getFileInfo, pathExists } from '../../../common/externalDependencies'; import { PythonEnvInfo } from '../../info'; import { areSameEnv, getEnvPath } from '../../info/env'; @@ -113,9 +112,6 @@ export class PythonEnvInfoCache extends PythonEnvsWatcher { const env = this.envs.splice(index, 1)[0]; this.fire({ old: env, new: undefined }); - reportInterpretersChanged([ - { path: getEnvPath(env.executable.filename, env.location).path, type: 'remove' }, - ]); }); } @@ -132,7 +128,6 @@ export class PythonEnvInfoCache extends PythonEnvsWatcher { this.fire({ old: e, new: undefined }); }); - reportInterpretersChanged([{ path: undefined, type: 'clear-all' }]); this.envs = []; return Promise.resolve(); } diff --git a/src/test/pythonEnvironments/base/locators/composite/envsCollectionService.unit.test.ts b/src/test/pythonEnvironments/base/locators/composite/envsCollectionService.unit.test.ts index 7bb70bead0a9..90dcb8345732 100644 --- a/src/test/pythonEnvironments/base/locators/composite/envsCollectionService.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/composite/envsCollectionService.unit.test.ts @@ -8,7 +8,6 @@ import * as sinon from 'sinon'; import { EventEmitter, Uri } from 'vscode'; import { FileChangeType } from '../../../../../client/common/platform/fileSystemWatcher'; import { createDeferred, createDeferredFromPromise, sleep } from '../../../../../client/common/utils/async'; -import * as proposedApi from '../../../../../client/proposedApi'; import { PythonEnvInfo, PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; import { buildEnvInfo } from '../../../../../client/pythonEnvironments/base/info/env'; import { @@ -31,7 +30,6 @@ import { assertEnvEqual, assertEnvsEqual } from '../envTestUtils'; suite('Python envs locator - Environments Collection', async () => { let collectionService: EnvsCollectionService; let storage: PythonEnvInfo[]; - let reportInterpretersChangedStub: sinon.SinonStub; const updatedName = 'updatedName'; @@ -129,7 +127,6 @@ suite('Python envs locator - Environments Collection', async () => { }, }); collectionService = new EnvsCollectionService(cache, parentLocator); - reportInterpretersChangedStub = sinon.stub(proposedApi, 'reportInterpretersChanged'); }); teardown(() => { @@ -377,7 +374,6 @@ suite('Python envs locator - Environments Collection', async () => { collectionService = new EnvsCollectionService(cache, parentLocator); const resolved = await collectionService.resolveEnv(env.executable.filename); assertEnvEqual(resolved, env); - sinon.assert.calledOnce(reportInterpretersChangedStub); }); test('resolveEnv() uses underlying locator if cache does not have up to date info for env', async () => { @@ -406,7 +402,6 @@ suite('Python envs locator - Environments Collection', async () => { collectionService = new EnvsCollectionService(cache, parentLocator); const resolved = await collectionService.resolveEnv(env.executable.filename); assertEnvEqual(resolved, resolvedViaLocator); - sinon.assert.calledOnce(reportInterpretersChangedStub); }); test('resolveEnv() uses underlying locator if cache does not have complete info for env', async () => { @@ -429,22 +424,6 @@ suite('Python envs locator - Environments Collection', async () => { collectionService = new EnvsCollectionService(cache, parentLocator); const resolved = await collectionService.resolveEnv(env.executable.filename); assertEnvEqual(resolved, resolvedViaLocator); - - const eventData = [ - { - path: path.join(TEST_LAYOUT_ROOT, 'doesNotExist'), - type: 'remove', - }, - - { - path: 'Resolved via locator', - type: 'add', - }, - ]; - eventData.forEach((d) => { - sinon.assert.calledWithExactly(reportInterpretersChangedStub, [d]); - }); - sinon.assert.callCount(reportInterpretersChangedStub, eventData.length); }); test('resolveEnv() adds env to cache after resolving using downstream locator', async () => { @@ -468,9 +447,6 @@ suite('Python envs locator - Environments Collection', async () => { const envs = collectionService.getEnvs(); expect(resolved?.hasLatestInfo).to.equal(true); assertEnvsEqual(envs, [resolved]); - sinon.assert.calledOnceWithExactly(reportInterpretersChangedStub, [ - { path: resolved?.executable.filename, type: 'add' }, - ]); }); test('Ensure events from downstream locators do not trigger new refreshes if a refresh is already scheduled', async () => { @@ -523,7 +499,5 @@ suite('Python envs locator - Environments Collection', async () => { events.sort((a, b) => (a.type && b.type ? a.type?.localeCompare(b.type) : 0)), downstreamEvents.sort((a, b) => (a.type && b.type ? a.type?.localeCompare(b.type) : 0)), ); - - sinon.assert.notCalled(reportInterpretersChangedStub); }); }); From 133aa66077659cab56e03cd145765a285e6bf7d8 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 14 Sep 2022 17:47:30 -0700 Subject: [PATCH 20/60] Add documentation --- src/client/proposedApiTypes.ts | 43 +++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index a1f4d9949b01..59a4e3f5986f 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -70,32 +70,69 @@ interface IEnvironmentLocatorAPI { * * Note this can be expensive so it's best to only use it if user manually triggers it. For * internal automatic triggers consider using {@link RefreshOptions.ifNotRefreshedAlready}. - * @param options Additonal options for refresh. + * @param options Additional options for refresh. * @param token A cancellation token that indicates a refresh is no longer needed. */ refreshEnvironment(options: RefreshOptions, token?: CancellationToken): Promise; } +/** + * Details about the environment. Note the type, name and environment path is known. + */ export interface Environment { pathID: UniquePathType; + /** + * Carries details about python executable. + */ executable: { + /** + * Path to the python interpreter/executable. Carries `undefined` in case an executable does not belong to the environment. + */ path: string | undefined; + /** + * Bitness if known at this moment. + */ bitness: Architecture | undefined; + /** + * Value of `sys.prefix` in sys module if known at this moment. + */ sysPrefix: string | undefined; }; + /** + * Carries details if it is an environment, otherwise `undefined` in case of global interpreters or something else. + */ environment: | { + /** + * Type of the environment. This never changes over time. + */ type: EnvType; + /** + * Name to the environment if any. + */ name: string | undefined; + /** + * Path to the environment folder. + */ folderPath: string; /** * Any specific workspace folder this environment is created for. */ workspaceFolder: Uri | undefined; + /** + * Tools/Plugins which created the environment or where it came from in the order. + * First value corresponds to the primary source, which never changes over time. + */ source: EnvSource[]; } | undefined; + /** + * Carries Python version information. + */ version: StandardVersionInfo & { + /** + * Value of `sys.version` in sys module if known at this moment. + */ sysVersion: string | undefined; }; } @@ -120,6 +157,10 @@ export interface ResolvedEnvironment { } export type ProgressNotificationEvent = { + /** + * * started: Fires when a refresh is started. + * * finished: Fires when a refresh is over. + */ stage: 'started' | 'finished'; }; From be4a3c526dd3282060c3dbace657ce968a004376 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 14 Sep 2022 18:03:24 -0700 Subject: [PATCH 21/60] Some updates --- src/client/proposedApi.ts | 4 ++-- src/client/proposedApiTypes.ts | 21 +++++++++------------ 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index a0a878c1070a..0b6c0e163d15 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -159,8 +159,8 @@ function convertCompleteEnvInfo(env: PythonEnvInfo): ResolvedEnvironment { ? { type: env.kind === PythonEnvKind.Conda ? 'Conda' : 'VirtualEnv', name: env.name, - folderPath: env.location, - workspaceFolder: env.searchLocation, + path: env.location, + workspaceFolderPath: env.searchLocation?.fsPath, source: [env.kind], } : undefined, diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index 59a4e3f5986f..bf80dedd8ca3 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -19,7 +19,7 @@ interface IEnvironmentAPI { * 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 + * @param resource : Uri of a file or workspace folder. */ getActiveEnvironment(resource?: Resource): Promise; /** @@ -31,8 +31,7 @@ interface IEnvironmentAPI { * Sets the active environment path for the python extension for the resource. Configuration target * will always be the workspace folder. * @param environment : Full path to environment folder or python executable for the environment. Can also pass the environment itself. - * @param resource : [optional] Uri of a file ro workspace to scope to a particular workspace - * folder. + * @param resource : [optional] File or workspace to scope to a particular workspace folder. */ setActiveEnvironment(environment: Environment | UniquePathType, resource?: Resource): Promise; /** @@ -45,8 +44,6 @@ interface IEnvironmentLocatorAPI { /** * Carries environments found by the extension at the time of fetching the property. To get complete list * `await` on promise returned by {@link waitOnRefresh()}. - * - * Only returns an environment if the final type, name and environment path is known. */ environments: readonly Environment[] | undefined; /** @@ -77,7 +74,7 @@ interface IEnvironmentLocatorAPI { } /** - * Details about the environment. Note the type, name and environment path is known. + * Details about the environment. Note the environment path, type and name never changes over time. */ export interface Environment { pathID: UniquePathType; @@ -99,12 +96,12 @@ export interface Environment { sysPrefix: string | undefined; }; /** - * Carries details if it is an environment, otherwise `undefined` in case of global interpreters or something else. + * Carries details if it is an environment, otherwise `undefined` in case of global interpreters and others. */ environment: | { /** - * Type of the environment. This never changes over time. + * Type of the environment. */ type: EnvType; /** @@ -114,20 +111,20 @@ export interface Environment { /** * Path to the environment folder. */ - folderPath: string; + path: string; /** * Any specific workspace folder this environment is created for. */ - workspaceFolder: Uri | undefined; + workspaceFolderPath: string | undefined; /** - * Tools/Plugins which created the environment or where it came from in the order. + * Tools/Plugins which created the environment or where it came from. * First value corresponds to the primary source, which never changes over time. */ source: EnvSource[]; } | undefined; /** - * Carries Python version information. + * Carries Python version information known at this moment. */ version: StandardVersionInfo & { /** From 7d6b91460aac444069e90e961a8f5a87bcb22680 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Wed, 14 Sep 2022 13:55:32 -0700 Subject: [PATCH 22/60] Simplify buffer decoder. (#19836) --- src/client/proposedApiTypes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index bf80dedd8ca3..e810a7c14b4d 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -113,11 +113,11 @@ export interface Environment { */ path: string; /** - * Any specific workspace folder this environment is created for. + * Path to any specific workspace folder this environment is created for. */ workspaceFolderPath: string | undefined; /** - * Tools/Plugins which created the environment or where it came from. + * Tools/plugins which created the environment or where it came from. * First value corresponds to the primary source, which never changes over time. */ source: EnvSource[]; From 81099673daf9962f5b506c5e0e381a556302946d Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 14 Sep 2022 21:10:31 -0700 Subject: [PATCH 23/60] Rename api --- src/client/proposedApi.ts | 4 ++-- src/client/proposedApiTypes.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index 0b6c0e163d15..5f50e22608c4 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -115,10 +115,10 @@ export function buildProposedApi( get onRefreshProgress() { return onProgress.event; }, - waitOnRefresh(): Promise | undefined { + waitForRefresh(): Promise | undefined { return discoveryApi.getRefreshPromise(); }, - async refreshEnvironment(options: RefreshOptions) { + async refreshEnvironments(options: RefreshOptions) { await discoveryApi.triggerRefresh(undefined, { ifNotTriggerredAlready: options.ifNotRefreshedAlready, }); diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index e810a7c14b4d..39cca7cb2936 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -43,7 +43,7 @@ interface IEnvironmentAPI { interface IEnvironmentLocatorAPI { /** * Carries environments found by the extension at the time of fetching the property. To get complete list - * `await` on promise returned by {@link waitOnRefresh()}. + * `await` on promise returned by {@link waitForRefresh()}. */ environments: readonly Environment[] | undefined; /** @@ -55,7 +55,7 @@ interface IEnvironmentLocatorAPI { * Returns a promise for the ongoing refresh. Returns `undefined` if there are no active * refreshes going on. */ - waitOnRefresh(): Promise | undefined; + waitForRefresh(): Promise | undefined; /** * Tracks discovery progress for current list of known environments, i.e when it starts, finishes or any other relevant * stage. @@ -70,7 +70,7 @@ interface IEnvironmentLocatorAPI { * @param options Additional options for refresh. * @param token A cancellation token that indicates a refresh is no longer needed. */ - refreshEnvironment(options: RefreshOptions, token?: CancellationToken): Promise; + refreshEnvironments(options: RefreshOptions, token?: CancellationToken): Promise; } /** From 54d88144de09950f72d793f56d78b9f329e82e24 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 15 Sep 2022 08:58:30 -0700 Subject: [PATCH 24/60] API sync review part 1 --- src/client/proposedApi.ts | 16 ++++++++-------- src/client/proposedApiTypes.ts | 32 ++++++++++++++++---------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index 5f50e22608c4..a2254ef7843f 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { ConfigurationTarget, EventEmitter } from 'vscode'; +import { ConfigurationTarget, EventEmitter, Uri } from 'vscode'; import { IDisposableRegistry, IInterpreterPathService } from './common/types'; import { IInterpreterService } from './interpreter/contracts'; import { IServiceContainer } from './ioc/types'; @@ -96,7 +96,7 @@ export function buildProposedApi( ); const proposed: IProposedExtensionAPI = { environment: { - async getActiveEnvironment(resource?: Resource) { + async fetchActiveEnvironment(resource?: Resource) { resource = resource && 'uri' in resource ? resource.uri : resource; const env = await interpreterService.getActiveInterpreter(resource); if (!env) { @@ -127,7 +127,7 @@ export function buildProposedApi( return onEnvironmentsChanged.event; }, }, - setActiveEnvironment(env: string | Environment, resource?: Resource): Promise { + updateActiveEnvironment(env: string | Environment, resource?: Resource): Promise { const path = typeof env !== 'string' ? env.pathID : env; resource = resource && 'uri' in resource ? resource.uri : resource; return interpreterPathService.update(resource, ConfigurationTarget.WorkspaceFolder, path); @@ -151,7 +151,7 @@ function convertCompleteEnvInfo(env: PythonEnvInfo): ResolvedEnvironment { return { pathID: getEnvPath(env.executable.filename, env.location).path, executable: { - path: env.executable.filename, + uri: Uri.file(env.executable.filename), bitness: env.arch, sysPrefix: env.executable.sysPrefix, }, @@ -159,8 +159,8 @@ function convertCompleteEnvInfo(env: PythonEnvInfo): ResolvedEnvironment { ? { type: env.kind === PythonEnvKind.Conda ? 'Conda' : 'VirtualEnv', name: env.name, - path: env.location, - workspaceFolderPath: env.searchLocation?.fsPath, + folderUri: Uri.file(env.location), + workspaceFolder: env.searchLocation, source: [env.kind], } : undefined, @@ -173,8 +173,8 @@ function convertEnvInfoAndGetReference(env: PythonEnvInfo): Environment { if (convertedEnv.executable.sysPrefix === '') { convertedEnv.executable.sysPrefix = undefined; } - if (convertedEnv.executable.path === 'python') { - convertedEnv.executable.path = undefined; + if (convertedEnv.executable.uri?.fsPath === 'python') { + convertedEnv.executable.uri = undefined; } if (convertedEnv.environment?.name === '') { convertedEnv.environment.name = undefined; diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index 39cca7cb2936..8cc3cf88c2c3 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -21,19 +21,19 @@ interface IEnvironmentAPI { * * @param resource : Uri of a file or workspace folder. */ - getActiveEnvironment(resource?: Resource): Promise; - /** - * Returns details for the given environment, or `undefined` if the env is invalid. - * @param environment : Environment whose details you need. Can also pass the full path to environment folder or python executable for the environment. - */ - resolveEnvironment(environment: Environment | UniquePathType): Promise; + fetchActiveEnvironment(resource?: Resource): Promise; /** * Sets the active environment path for the python extension for the resource. Configuration target * will always be the workspace folder. * @param environment : Full path to environment folder or python executable for the environment. Can also pass the environment itself. * @param resource : [optional] File or workspace to scope to a particular workspace folder. */ - setActiveEnvironment(environment: Environment | UniquePathType, resource?: Resource): Promise; + updateActiveEnvironment(environment: Environment | UniquePathType, resource?: Resource): Promise; + /** + * Returns details for the given environment, or `undefined` if the env is invalid. + * @param environment : Environment whose details you need. Can also pass the full path to environment folder or python executable for the environment. + */ + resolveEnvironment(environment: Environment | UniquePathType): Promise; /** * Carries the API necessary for locating environments. */ @@ -42,8 +42,8 @@ interface IEnvironmentAPI { interface IEnvironmentLocatorAPI { /** - * Carries environments found by the extension at the time of fetching the property. To get complete list - * `await` on promise returned by {@link waitForRefresh()}. + * Carries environments found by the extension at the time of fetching the property. Note a refresh might be going on + * so this may not be the complete list. To get complete list `await` on promise returned by {@link waitForRefresh()}. */ environments: readonly Environment[] | undefined; /** @@ -74,7 +74,7 @@ interface IEnvironmentLocatorAPI { } /** - * Details about the environment. Note the environment path, type and name never changes over time. + * Details about the environment. Note the environment folder, type and name never changes over time. */ export interface Environment { pathID: UniquePathType; @@ -83,9 +83,9 @@ export interface Environment { */ executable: { /** - * Path to the python interpreter/executable. Carries `undefined` in case an executable does not belong to the environment. + * Uri of the python interpreter/executable. Carries `undefined` in case an executable does not belong to the environment. */ - path: string | undefined; + uri: Uri | undefined; /** * Bitness if known at this moment. */ @@ -109,13 +109,13 @@ export interface Environment { */ name: string | undefined; /** - * Path to the environment folder. + * Uri of the environment folder. */ - path: string; + folderUri: Uri; /** - * Path to any specific workspace folder this environment is created for. + * Any specific workspace folder this environment is created for. */ - workspaceFolderPath: string | undefined; + workspaceFolder: Uri | undefined; /** * Tools/plugins which created the environment or where it came from. * First value corresponds to the primary source, which never changes over time. From c1ac005d917e939d2c422d73f599a21c229c52c0 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 15 Sep 2022 15:16:27 -0700 Subject: [PATCH 25/60] API sync review part 2 - update refresh API --- src/client/proposedApi.ts | 22 +++++++---- src/client/proposedApiTypes.ts | 37 ++++++++++++------- src/client/pythonEnvironments/api.ts | 5 +++ src/client/pythonEnvironments/base/locator.ts | 1 + .../composite/envsCollectionService.ts | 3 ++ 5 files changed, 48 insertions(+), 20 deletions(-) diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index a2254ef7843f..dfc6e13a69c8 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -11,11 +11,12 @@ import { EnvironmentsChangedParams, IProposedExtensionAPI, ResolvedEnvironment, - ProgressNotificationEvent, UniquePathType, PythonVersionInfo, RefreshOptions, Resource, + RefreshStateValues, + RefreshState, } from './proposedApiTypes'; import { PythonEnvInfo, PythonEnvKind, virtualEnvKinds } from './pythonEnvironments/base/info'; import { getEnvPath } from './pythonEnvironments/base/info/env'; @@ -25,7 +26,7 @@ const onDidActiveInterpreterChangedEvent = new EventEmitter(); +const onProgress = new EventEmitter(); const onEnvironmentsChanged = new EventEmitter(); const environmentsReference = new Map(); @@ -74,10 +75,10 @@ export function buildProposedApi( disposables.push( discoveryApi.onProgress((e) => { if (e.stage === ProgressReportStage.discoveryStarted) { - onProgress.fire({ stage: 'started' }); + onProgress.fire({ state: RefreshStateValues.started }); } if (e.stage === ProgressReportStage.discoveryFinished) { - onProgress.fire({ stage: 'finished' }); + onProgress.fire({ state: RefreshStateValues.finished }); } }), discoveryApi.onChanged((e) => { @@ -112,11 +113,18 @@ export function buildProposedApi( get environments(): Environment[] { return discoveryApi.getEnvs().map((e) => convertEnvInfoAndGetReference(e)); }, - get onRefreshProgress() { + get onDidChangeRefreshState() { return onProgress.event; }, - waitForRefresh(): Promise | undefined { - return discoveryApi.getRefreshPromise(); + get refreshState(): RefreshState { + switch (discoveryApi.refreshState) { + case ProgressReportStage.discoveryStarted: + return { state: RefreshStateValues.started }; + case ProgressReportStage.discoveryFinished: + return { state: RefreshStateValues.finished }; + default: + return { state: RefreshStateValues.started }; + } }, async refreshEnvironments(options: RefreshOptions) { await discoveryApi.triggerRefresh(undefined, { diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index 8cc3cf88c2c3..cc74e9f7aac5 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -42,8 +42,7 @@ interface IEnvironmentAPI { interface IEnvironmentLocatorAPI { /** - * Carries environments found by the extension at the time of fetching the property. Note a refresh might be going on - * so this may not be the complete list. To get complete list `await` on promise returned by {@link waitForRefresh()}. + * Carries environments found by the extension at the time of fetching the property. Note a refresh might be going on so this may not be the complete list. To wait on complete list use {@link refreshState()} and {@link onDidChangeRefreshState}. */ environments: readonly Environment[] | undefined; /** @@ -52,15 +51,14 @@ interface IEnvironmentLocatorAPI { */ onDidChangeEnvironments: Event; /** - * Returns a promise for the ongoing refresh. Returns `undefined` if there are no active - * refreshes going on. + * Returns the last known refresh state, i.e whether it started, finished, or any other relevant state. */ - waitForRefresh(): Promise | undefined; + refreshState: RefreshState; /** - * Tracks discovery progress for current list of known environments, i.e when it starts, finishes or any other relevant - * stage. + * Tracks refresh progress for current list of known environments, i.e when it starts, finishes or any other relevant + * state. */ - readonly onRefreshProgress: Event; + readonly onDidChangeRefreshState: Event; /** * This API will re-trigger environment discovery. If there is a refresh already going on then it * returns the promise for that refresh. @@ -153,13 +151,26 @@ export interface ResolvedEnvironment { version: PythonVersionInfo; } -export type ProgressNotificationEvent = { +export type RefreshState = { + state: RefreshStateValues; +}; + +/** + * Value of the enum indicates which states comes before when a refresh takes place. + */ +export enum RefreshStateValues { /** - * * started: Fires when a refresh is started. - * * finished: Fires when a refresh is over. + * When a refresh is started. */ - stage: 'started' | 'finished'; -}; + started = 0, + + // ...there can be more intimidatory states + + /** + * When a refresh is over. + */ + finished = 1, +} /** * Uri of a file inside a workspace or workspace folder itself. diff --git a/src/client/pythonEnvironments/api.ts b/src/client/pythonEnvironments/api.ts index b9c3152a0b67..a2065c30b740 100644 --- a/src/client/pythonEnvironments/api.ts +++ b/src/client/pythonEnvironments/api.ts @@ -6,6 +6,7 @@ import { GetRefreshEnvironmentsOptions, IDiscoveryAPI, ProgressNotificationEvent, + ProgressReportStage, PythonLocatorQuery, TriggerRefreshOptions, } from './base/locator'; @@ -33,6 +34,10 @@ class PythonEnvironments implements IDiscoveryAPI { return this.locator.onProgress; } + public get refreshState(): ProgressReportStage { + return this.locator.refreshState; + } + public getRefreshPromise(options?: GetRefreshEnvironmentsOptions) { return this.locator.getRefreshPromise(options); } diff --git a/src/client/pythonEnvironments/base/locator.ts b/src/client/pythonEnvironments/base/locator.ts index c0d1cd23991c..687348964891 100644 --- a/src/client/pythonEnvironments/base/locator.ts +++ b/src/client/pythonEnvironments/base/locator.ts @@ -201,6 +201,7 @@ export type TriggerRefreshOptions = { }; export interface IDiscoveryAPI { + readonly refreshState: ProgressReportStage; /** * 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/locators/composite/envsCollectionService.ts b/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts index 0e1466bc385d..ca7c93b1c269 100644 --- a/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts +++ b/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts @@ -43,6 +43,8 @@ export class EnvsCollectionService extends PythonEnvsWatcher(); + public refreshState = ProgressReportStage.discoveryFinished; + public get onProgress(): Event { return this.progress.event; } @@ -70,6 +72,7 @@ export class EnvsCollectionService extends PythonEnvsWatcher { + this.refreshState = event.stage; // Resolve progress promise indicating the stage has been reached. this.progressPromises.get(event.stage)?.resolve(); this.progressPromises.delete(event.stage); From d32f6e6f1ec1de7b4160fc5ae7d61236cac55860 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 15 Sep 2022 16:23:24 -0700 Subject: [PATCH 26/60] Update activeEnvironment API --- src/client/proposedApi.ts | 40 +++++++++-------- src/client/proposedApiTypes.ts | 81 +++++++++++++++++++--------------- 2 files changed, 67 insertions(+), 54 deletions(-) diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index dfc6e13a69c8..1c4b76110ffc 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -15,7 +15,7 @@ import { PythonVersionInfo, RefreshOptions, Resource, - RefreshStateValues, + RefreshStateValue, RefreshState, } from './proposedApiTypes'; import { PythonEnvInfo, PythonEnvKind, virtualEnvKinds } from './pythonEnvironments/base/info'; @@ -75,10 +75,10 @@ export function buildProposedApi( disposables.push( discoveryApi.onProgress((e) => { if (e.stage === ProgressReportStage.discoveryStarted) { - onProgress.fire({ state: RefreshStateValues.started }); + onProgress.fire({ state: RefreshStateValue.started }); } if (e.stage === ProgressReportStage.discoveryFinished) { - onProgress.fire({ state: RefreshStateValues.finished }); + onProgress.fire({ state: RefreshStateValue.finished }); } }), discoveryApi.onChanged((e) => { @@ -97,13 +97,21 @@ export function buildProposedApi( ); const proposed: IProposedExtensionAPI = { environment: { - async fetchActiveEnvironment(resource?: Resource) { - resource = resource && 'uri' in resource ? resource.uri : resource; - const env = await interpreterService.getActiveInterpreter(resource); - if (!env) { - return undefined; - } - return resolveEnvironment(getEnvPath(env.path, env.envPath).path, discoveryApi); + activeEnvironment: { + async fetch(resource?: Resource) { + resource = resource && 'uri' in resource ? resource.uri : resource; + const env = await interpreterService.getActiveInterpreter(resource); + if (!env) { + return undefined; + } + return resolveEnvironment(getEnvPath(env.path, env.envPath).path, discoveryApi); + }, + update(env: string | Environment, resource?: Resource): Promise { + const path = typeof env !== 'string' ? env.pathID : env; + resource = resource && 'uri' in resource ? resource.uri : resource; + return interpreterPathService.update(resource, ConfigurationTarget.WorkspaceFolder, path); + }, + onDidChange: onDidActiveInterpreterChangedEvent.event, }, resolveEnvironment: (env: string | Environment) => { const path = typeof env !== 'string' ? env.pathID : env; @@ -119,11 +127,11 @@ export function buildProposedApi( get refreshState(): RefreshState { switch (discoveryApi.refreshState) { case ProgressReportStage.discoveryStarted: - return { state: RefreshStateValues.started }; + return { state: RefreshStateValue.started }; case ProgressReportStage.discoveryFinished: - return { state: RefreshStateValues.finished }; + return { state: RefreshStateValue.finished }; default: - return { state: RefreshStateValues.started }; + return { state: RefreshStateValue.started }; } }, async refreshEnvironments(options: RefreshOptions) { @@ -135,12 +143,6 @@ export function buildProposedApi( return onEnvironmentsChanged.event; }, }, - updateActiveEnvironment(env: string | Environment, resource?: Resource): Promise { - const path = typeof env !== 'string' ? env.pathID : env; - resource = resource && 'uri' in resource ? resource.uri : resource; - return interpreterPathService.update(resource, ConfigurationTarget.WorkspaceFolder, path); - }, - onDidChangeActiveEnvironment: onDidActiveInterpreterChangedEvent.event, }, }; return proposed; diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index cc74e9f7aac5..6fd766e74289 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -11,38 +11,48 @@ export interface IProposedExtensionAPI { interface IEnvironmentAPI { /** - * This event is triggered when the active environment changes. - */ - onDidChangeActiveEnvironment: Event; - /** - * Returns the environment selected by the user or as in the settings. 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 folder. + * Carries the API to tracks the selected environment by the user for a workspace. */ - fetchActiveEnvironment(resource?: Resource): Promise; + activeEnvironment: IActiveEnvironmentAPI; /** - * Sets the active environment path for the python extension for the resource. Configuration target - * will always be the workspace folder. - * @param environment : Full path to environment folder or python executable for the environment. Can also pass the environment itself. - * @param resource : [optional] File or workspace to scope to a particular workspace folder. + * Carries the API necessary for locating environments. */ - updateActiveEnvironment(environment: Environment | UniquePathType, resource?: Resource): Promise; + locator: IEnvironmentLocatorAPI; /** * Returns details for the given environment, or `undefined` if the env is invalid. - * @param environment : Environment whose details you need. Can also pass the full path to environment folder or python executable for the environment. + * @param environment : Environment whose details you need. Can also pass the full path to environment folder + * or python executable for the environment. */ resolveEnvironment(environment: Environment | UniquePathType): Promise; +} + +interface IActiveEnvironmentAPI { /** - * Carries the API necessary for locating environments. + * This event is triggered when the active environment changes. */ - locator: IEnvironmentLocatorAPI; + onDidChange: Event; + /** + * Returns the environment selected. 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. + * Uses the cache by default, otherwise fetches full information about the environment. + * @param resource : Uri of a file or workspace folder. + */ + fetch(resource?: Resource): Promise; + /** + * Sets the active environment path for the python extension for the resource. Configuration target will always + * be the workspace folder. + * @param environment : Full path to environment folder or python executable for the environment. Can also pass + * the environment itself. + * @param resource : [optional] File or workspace to scope to a particular workspace folder. + */ + update(environment: Environment | UniquePathType, resource?: Resource): Promise; } interface IEnvironmentLocatorAPI { /** - * Carries environments found by the extension at the time of fetching the property. Note a refresh might be going on so this may not be the complete list. To wait on complete list use {@link refreshState()} and {@link onDidChangeRefreshState}. + * Carries environments found by the extension at the time of fetching the property. Note a refresh might be + * going on so this may not be the complete list. To wait on complete list use {@link refreshState()} and + * {@link onDidChangeRefreshState}. */ environments: readonly Environment[] | undefined; /** @@ -55,16 +65,16 @@ interface IEnvironmentLocatorAPI { */ refreshState: RefreshState; /** - * Tracks refresh progress for current list of known environments, i.e when it starts, finishes or any other relevant - * state. + * Tracks refresh progress for current list of known environments, i.e when it starts, finishes or any other + * relevant state. */ readonly onDidChangeRefreshState: Event; /** - * This API will re-trigger environment discovery. If there is a refresh already going on then it - * returns the promise for that refresh. + * This API will re-trigger environment discovery. If there is a refresh already going on then it returns the + * promise for that refresh. * - * Note this can be expensive so it's best to only use it if user manually triggers it. For - * internal automatic triggers consider using {@link RefreshOptions.ifNotRefreshedAlready}. + * Note this can be expensive so it's best to only use it if user manually triggers it. For internal automatic + * triggers consider using {@link RefreshOptions.ifNotRefreshedAlready}. * @param options Additional options for refresh. * @param token A cancellation token that indicates a refresh is no longer needed. */ @@ -81,7 +91,8 @@ export interface Environment { */ executable: { /** - * Uri of the python interpreter/executable. Carries `undefined` in case an executable does not belong to the environment. + * Uri of the python interpreter/executable. Carries `undefined` in case an executable does not belong to + * the environment. */ uri: Uri | undefined; /** @@ -115,8 +126,8 @@ export interface Environment { */ workspaceFolder: Uri | undefined; /** - * Tools/plugins which created the environment or where it came from. - * First value corresponds to the primary source, which never changes over time. + * Tools/plugins which created the environment or where it came from. First value in array corresponds + * to the primary source, which never changes over time. */ source: EnvSource[]; } @@ -152,13 +163,13 @@ export interface ResolvedEnvironment { } export type RefreshState = { - state: RefreshStateValues; + state: RefreshStateValue; }; /** * Value of the enum indicates which states comes before when a refresh takes place. */ -export enum RefreshStateValues { +export enum RefreshStateValue { /** * When a refresh is started. */ @@ -178,9 +189,9 @@ export enum RefreshStateValues { 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. + * 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; @@ -204,8 +215,8 @@ export interface ActiveEnvironmentChangedParams { export type RefreshOptions = { /** - * Only trigger a refresh if not triggered already for this session. Useful for making sure env - * list is up-to-date when extension starts up. + * Only trigger a refresh if not triggered already for this session. Useful for making sure env list is + * up-to-date when extension starts up. * * After that users can use extension specific UI to refresh environments when needed. */ From 7676a49549cd8f2910e2d7f3d93a291bf5d98dd0 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Thu, 15 Sep 2022 16:52:14 -0700 Subject: [PATCH 27/60] Add a few tests --- src/client/apiTypes.ts | 136 ------ src/client/extension.ts | 3 +- src/client/proposedApi.ts | 7 +- src/client/proposedApiTypes.ts | 4 +- src/test/proposedApi.unit.test.ts | 729 +++++++++++++++--------------- 5 files changed, 377 insertions(+), 502 deletions(-) diff --git a/src/client/apiTypes.ts b/src/client/apiTypes.ts index 6361a75edb48..a10fd2dccb96 100644 --- a/src/client/apiTypes.ts +++ b/src/client/apiTypes.ts @@ -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. @@ -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; -} - -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/proposedApi.ts b/src/client/proposedApi.ts index 1c4b76110ffc..4dabde2c6392 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. @@ -30,10 +31,10 @@ const onProgress = new EventEmitter(); const onEnvironmentsChanged = new EventEmitter(); const environmentsReference = new Map(); -class EnvironmentReference implements Environment { +export class EnvironmentReference implements Environment { readonly pathID: string; - constructor(private internal: Environment) { + constructor(public internal: Environment) { this.pathID = internal.pathID; } @@ -156,7 +157,7 @@ async function resolveEnvironment(path: string, discoveryApi: IDiscoveryAPI): Pr return getEnvReference(convertCompleteEnvInfo(env)) as ResolvedEnvironment; } -function convertCompleteEnvInfo(env: PythonEnvInfo): ResolvedEnvironment { +export function convertCompleteEnvInfo(env: PythonEnvInfo): ResolvedEnvironment { const version = { ...env.version, sysVersion: env.version.sysVersion }; return { pathID: getEnvPath(env.executable.filename, env.location).path, diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index 6fd766e74289..9fe56e4102e3 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -11,7 +11,7 @@ export interface IProposedExtensionAPI { interface IEnvironmentAPI { /** - * Carries the API to tracks the selected environment by the user for a workspace. + * Carries the API to track the selected environment by the user for a workspace. */ activeEnvironment: IActiveEnvironmentAPI; /** @@ -61,7 +61,7 @@ interface IEnvironmentLocatorAPI { */ onDidChangeEnvironments: Event; /** - * Returns the last known refresh state, i.e whether it started, finished, or any other relevant state. + * Returns the last known state in the refresh, i.e whether it started, finished, or any other relevant state. */ refreshState: RefreshState; /** diff --git a/src/test/proposedApi.unit.test.ts b/src/test/proposedApi.unit.test.ts index ad4cdc904a22..09062faa7783 100644 --- a/src/test/proposedApi.unit.test.ts +++ b/src/test/proposedApi.unit.test.ts @@ -2,22 +2,31 @@ // Licensed under the MIT License. import * as typemoq from 'typemoq'; -import { assert, expect } from 'chai'; -import { ConfigurationTarget, Uri, Event } from 'vscode'; -import { EnvironmentDetails, IProposedExtensionAPI } from '../client/apiTypes'; -import { IInterpreterPathService } from '../client/common/types'; +import { assert } from 'chai'; +import { Uri, Event, EventEmitter } from 'vscode'; +import { IDisposableRegistry, IInterpreterPathService } from '../client/common/types'; import { IInterpreterService } from '../client/interpreter/contracts'; import { IServiceContainer } from '../client/ioc/types'; -import { buildProposedApi } from '../client/proposedApi'; +import { + buildProposedApi, + convertCompleteEnvInfo, + EnvironmentReference, + reportActiveInterpreterChanged, +} from '../client/proposedApi'; import { IDiscoveryAPI, ProgressNotificationEvent, ProgressReportStage, } from '../client/pythonEnvironments/base/locator'; import { PythonEnvironment } from '../client/pythonEnvironments/info'; -import { PythonEnvKind, PythonEnvSource } from '../client/pythonEnvironments/base/info'; -import { Architecture } from '../client/common/utils/platform'; import { buildEnvInfo } from '../client/pythonEnvironments/base/info/env'; +import { + ActiveEnvironmentChangedParams, + IProposedExtensionAPI, + RefreshState, + RefreshStateValue, +} from '../client/proposedApiTypes'; +import { sleep } from './core'; suite('Proposed Extension API', () => { let serviceContainer: typemoq.IMock; @@ -25,52 +34,49 @@ suite('Proposed Extension API', () => { let interpreterPathService: typemoq.IMock; let interpreterService: typemoq.IMock; let onDidExecutionEvent: Event; - let onRefreshProgress: Event; + let onDidChangeRefreshState: EventEmitter; let proposed: IProposedExtensionAPI; setup(() => { - serviceContainer = typemoq.Mock.ofType(undefined, typemoq.MockBehavior.Strict); - discoverAPI = typemoq.Mock.ofType(undefined, typemoq.MockBehavior.Strict); - interpreterPathService = typemoq.Mock.ofType(undefined, typemoq.MockBehavior.Strict); - interpreterService = typemoq.Mock.ofType(undefined, typemoq.MockBehavior.Strict); + serviceContainer = typemoq.Mock.ofType(); + discoverAPI = typemoq.Mock.ofType(); + interpreterPathService = typemoq.Mock.ofType(); + interpreterService = typemoq.Mock.ofType(); onDidExecutionEvent = typemoq.Mock.ofType>().object; - onRefreshProgress = typemoq.Mock.ofType>().object; + onDidChangeRefreshState = new EventEmitter(); interpreterService.setup((i) => i.onDidChangeInterpreterConfiguration).returns(() => onDidExecutionEvent); serviceContainer.setup((s) => s.get(IInterpreterPathService)).returns(() => interpreterPathService.object); serviceContainer.setup((s) => s.get(IInterpreterService)).returns(() => interpreterService.object); + serviceContainer.setup((s) => s.get(IDisposableRegistry)).returns(() => []); - discoverAPI.setup((d) => d.onProgress).returns(() => onRefreshProgress); + discoverAPI.setup((d) => d.onProgress).returns(() => onDidChangeRefreshState.event); proposed = buildProposedApi(discoverAPI.object, serviceContainer.object); }); - test('Provide a callback for tracking refresh progress', async () => { - assert.deepEqual(proposed.environment.onRefreshProgress, onRefreshProgress); - }); - - test('Provide a callback which is called when execution details changes', async () => { - assert.deepEqual(onDidExecutionEvent, proposed.environment.onDidChangeExecutionDetails); + test('Provides a event for tracking refresh progress', async () => { + const events: RefreshState[] = []; + proposed.environment.locator.onDidChangeRefreshState((e) => { + events.push(e); + }); + onDidChangeRefreshState.fire({ stage: ProgressReportStage.discoveryStarted }); + onDidChangeRefreshState.fire({ stage: ProgressReportStage.allPathsDiscovered }); + onDidChangeRefreshState.fire({ stage: ProgressReportStage.discoveryFinished }); + await sleep(1); + + assert.deepEqual(events, [{ state: RefreshStateValue.started }, { state: RefreshStateValue.finished }]); }); - test('getExecutionDetails: No resource', async () => { - const pythonPath = 'this/is/a/test/path'; - interpreterService - .setup((c) => c.getActiveInterpreter(undefined)) - .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); - const actual = await proposed.environment.getExecutionDetails(); - assert.deepEqual(actual, { execCommand: [pythonPath] }); - }); - - test('getExecutionDetails: With resource', async () => { - const resource = Uri.file(__filename); - const pythonPath = 'this/is/a/test/path'; - interpreterService - .setup((c) => c.getActiveInterpreter(resource)) - .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); - const actual = await proposed.environment.getExecutionDetails(resource); - assert.deepEqual(actual, { execCommand: [pythonPath] }); + test('Provide an event to track when active environment details change', async () => { + const events: ActiveEnvironmentChangedParams[] = []; + proposed.environment.activeEnvironment.onDidChange((e) => { + events.push(e); + }); + reportActiveInterpreterChanged({ pathID: 'path/to/environment', resource: undefined }); + await sleep(1); + assert.deepEqual(events, [{ pathID: 'path/to/environment', resource: undefined }]); }); test('getActiveInterpreterPath: No resource', async () => { @@ -78,329 +84,332 @@ suite('Proposed Extension API', () => { interpreterService .setup((c) => c.getActiveInterpreter(undefined)) .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); - const actual = await proposed.environment.getActiveEnvironmentPath(); - assert.deepEqual(actual, { path: pythonPath, pathType: 'interpreterPath' }); - }); - test('getActiveInterpreterPath: With resource', async () => { - const resource = Uri.file(__filename); - const pythonPath = 'this/is/a/test/path'; - interpreterService - .setup((c) => c.getActiveInterpreter(resource)) - .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); - const actual = await proposed.environment.getActiveEnvironmentPath(resource); - assert.deepEqual(actual, { path: pythonPath, pathType: 'interpreterPath' }); - }); - - test('getInterpreterDetails: no discovered python', async () => { - discoverAPI.setup((d) => d.getEnvs()).returns(() => []); - discoverAPI.setup((p) => p.resolveEnv(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); - - const pythonPath = 'this/is/a/test/path (without cache)'; - const actual = await proposed.environment.getEnvironmentDetails(pythonPath); - expect(actual).to.be.equal(undefined); - }); - - test('getInterpreterDetails: no discovered python (with cache)', async () => { - discoverAPI.setup((d) => d.getEnvs()).returns(() => []); - discoverAPI.setup((p) => p.resolveEnv(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); - - const pythonPath = 'this/is/a/test/path'; - const actual = await proposed.environment.getEnvironmentDetails(pythonPath, { useCache: true }); - expect(actual).to.be.equal(undefined); - }); - - test('getInterpreterDetails: without cache', async () => { - const pythonPath = 'this/is/a/test/path'; - - const expected: EnvironmentDetails = { - interpreterPath: pythonPath, - version: ['3', '9', '0'], - environmentType: [PythonEnvKind.System], - metadata: { - sysPrefix: 'prefix/path', - bitness: Architecture.x64, - project: Uri.file('path/to/project'), - }, - envFolderPath: undefined, - }; - - discoverAPI.setup((d) => d.getEnvs()).returns(() => []); - discoverAPI - .setup((p) => p.resolveEnv(pythonPath)) - .returns(() => - Promise.resolve( - buildEnvInfo({ - executable: pythonPath, - version: { - major: 3, - minor: 9, - micro: 0, - }, - kind: PythonEnvKind.System, - arch: Architecture.x64, - sysPrefix: 'prefix/path', - searchLocation: Uri.file('path/to/project'), - }), - ), - ); - - const actual = await proposed.environment.getEnvironmentDetails(pythonPath, { useCache: false }); - expect(actual).to.be.deep.equal(expected); - }); - - test('getInterpreterDetails: from cache', async () => { - const pythonPath = 'this/is/a/test/path'; - - const expected: EnvironmentDetails = { - interpreterPath: pythonPath, - version: ['3', '9', '0'], - environmentType: [PythonEnvKind.System], - metadata: { - sysPrefix: 'prefix/path', - bitness: Architecture.x64, - project: undefined, - }, - envFolderPath: undefined, - }; - - discoverAPI - .setup((d) => d.getEnvs()) - .returns(() => [ - { - executable: { - filename: pythonPath, - ctime: 1, - mtime: 2, - sysPrefix: 'prefix/path', - }, - version: { - major: 3, - minor: 9, - micro: 0, - }, - kind: PythonEnvKind.System, - arch: Architecture.x64, - name: '', - location: '', - source: [PythonEnvSource.PathEnvVar], - distro: { - org: '', - }, - }, - ]); - discoverAPI - .setup((p) => p.resolveEnv(pythonPath)) - .returns(() => - Promise.resolve( - buildEnvInfo({ - executable: pythonPath, - version: { - major: 3, - minor: 9, - micro: 0, - }, - kind: PythonEnvKind.System, - arch: Architecture.x64, - sysPrefix: 'prefix/path', - }), - ), - ); - - const actual = await proposed.environment.getEnvironmentDetails(pythonPath, { useCache: true }); - expect(actual).to.be.deep.equal(expected); - }); - - test('getInterpreterDetails: cache miss', async () => { - const pythonPath = 'this/is/a/test/path'; - - const expected: EnvironmentDetails = { - interpreterPath: pythonPath, - version: ['3', '9', '0'], - environmentType: [PythonEnvKind.System], - metadata: { - sysPrefix: 'prefix/path', - bitness: Architecture.x64, - project: undefined, - }, - envFolderPath: undefined, - }; - - // Force this API to return empty to cause a cache miss. - discoverAPI.setup((d) => d.getEnvs()).returns(() => []); - discoverAPI - .setup((p) => p.resolveEnv(pythonPath)) - .returns(() => - Promise.resolve( - buildEnvInfo({ - executable: pythonPath, - version: { - major: 3, - minor: 9, - micro: 0, - }, - kind: PythonEnvKind.System, - arch: Architecture.x64, - sysPrefix: 'prefix/path', - }), - ), - ); - - const actual = await proposed.environment.getEnvironmentDetails(pythonPath, { useCache: true }); - expect(actual).to.be.deep.equal(expected); - }); - - test('getInterpreterPaths: no pythons found', async () => { - discoverAPI.setup((d) => d.getEnvs()).returns(() => []); - const actual = await proposed.environment.getEnvironmentPaths(); - expect(actual).to.be.deep.equal([]); + const env = buildEnvInfo({ executable: pythonPath }); + discoverAPI.setup((d) => d.resolveEnv(pythonPath)).returns(() => Promise.resolve(env)); + const actual = await proposed.environment.activeEnvironment.fetch(); + assert.deepEqual((actual as EnvironmentReference).internal, convertCompleteEnvInfo(env)); }); - test('getInterpreterPaths: python found', async () => { - discoverAPI - .setup((d) => d.getEnvs()) - .returns(() => [ - { - executable: { - filename: 'this/is/a/test/python/path1', - ctime: 1, - mtime: 2, - sysPrefix: 'prefix/path', - }, - version: { - major: 3, - minor: 9, - micro: 0, - }, - kind: PythonEnvKind.System, - arch: Architecture.x64, - name: '', - location: '', - source: [PythonEnvSource.PathEnvVar], - distro: { - org: '', - }, - }, - { - executable: { - filename: 'this/is/a/test/python/path2', - ctime: 1, - mtime: 2, - sysPrefix: 'prefix/path', - }, - version: { - major: 3, - minor: 10, - micro: 0, - }, - kind: PythonEnvKind.Venv, - arch: Architecture.x64, - name: '', - location: '', - source: [PythonEnvSource.PathEnvVar], - distro: { - org: '', - }, - }, - ]); - const actual = await proposed.environment.getEnvironmentPaths(); - expect(actual?.map((a) => a.path)).to.be.deep.equal([ - 'this/is/a/test/python/path1', - 'this/is/a/test/python/path2', - ]); - }); - - test('setActiveInterpreter: no resource', async () => { - interpreterPathService - .setup((i) => i.update(undefined, ConfigurationTarget.WorkspaceFolder, 'this/is/a/test/python/path')) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.once()); - - await proposed.environment.setActiveEnvironment('this/is/a/test/python/path'); - - interpreterPathService.verifyAll(); - }); - test('setActiveInterpreter: with resource', async () => { - const resource = Uri.parse('a'); - interpreterPathService - .setup((i) => i.update(resource, ConfigurationTarget.WorkspaceFolder, 'this/is/a/test/python/path')) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.once()); - - await proposed.environment.setActiveEnvironment('this/is/a/test/python/path', resource); - - interpreterPathService.verifyAll(); - }); - - test('refreshInterpreters: common scenario', async () => { - discoverAPI - .setup((d) => d.triggerRefresh(undefined, undefined)) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.once()); - discoverAPI - .setup((d) => d.getEnvs()) - .returns(() => [ - { - executable: { - filename: 'this/is/a/test/python/path1', - ctime: 1, - mtime: 2, - sysPrefix: 'prefix/path', - }, - version: { - major: 3, - minor: 9, - micro: 0, - }, - kind: PythonEnvKind.System, - arch: Architecture.x64, - name: '', - location: 'this/is/a/test/python/path1/folder', - source: [PythonEnvSource.PathEnvVar], - distro: { - org: '', - }, - }, - { - executable: { - filename: 'this/is/a/test/python/path2', - ctime: 1, - mtime: 2, - sysPrefix: 'prefix/path', - }, - version: { - major: 3, - minor: 10, - micro: 0, - }, - kind: PythonEnvKind.Venv, - arch: Architecture.x64, - name: '', - location: '', - source: [PythonEnvSource.PathEnvVar], - distro: { - org: '', - }, - }, - ]); - - const actual = await proposed.environment.refreshEnvironment(); - expect(actual).to.be.deep.equal([ - { path: 'this/is/a/test/python/path1/folder', pathType: 'envFolderPath' }, - { path: 'this/is/a/test/python/path2', pathType: 'interpreterPath' }, - ]); - discoverAPI.verifyAll(); - }); - - test('getRefreshPromise: common scenario', () => { - const expected = Promise.resolve(); - discoverAPI - .setup((d) => d.getRefreshPromise(typemoq.It.isValue({ stage: ProgressReportStage.allPathsDiscovered }))) - .returns(() => expected); - const actual = proposed.environment.getRefreshPromise({ stage: ProgressReportStage.allPathsDiscovered }); - - // We are comparing instances here, they should be the same instance. - // So '==' is ok here. - // eslint-disable-next-line eqeqeq - expect(actual == expected).is.equal(true); - }); + // test('getActiveInterpreterPath: With resource', async () => { + // const resource = Uri.file(__filename); + // const pythonPath = 'this/is/a/test/path'; + // interpreterService + // .setup((c) => c.getActiveInterpreter(resource)) + // .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); + // const actual = await proposed.environment.getActiveEnvironmentPath(resource); + // assert.deepEqual(actual, { path: pythonPath, pathType: 'interpreterPath' }); + // }); + + // test('getInterpreterDetails: no discovered python', async () => { + // discoverAPI.setup((d) => d.getEnvs()).returns(() => []); + // discoverAPI.setup((p) => p.resolveEnv(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + + // const pythonPath = 'this/is/a/test/path (without cache)'; + // const actual = await proposed.environment.getEnvironmentDetails(pythonPath); + // expect(actual).to.be.equal(undefined); + // }); + + // test('getInterpreterDetails: no discovered python (with cache)', async () => { + // discoverAPI.setup((d) => d.getEnvs()).returns(() => []); + // discoverAPI.setup((p) => p.resolveEnv(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + + // const pythonPath = 'this/is/a/test/path'; + // const actual = await proposed.environment.getEnvironmentDetails(pythonPath, { useCache: true }); + // expect(actual).to.be.equal(undefined); + // }); + + // test('getInterpreterDetails: without cache', async () => { + // const pythonPath = 'this/is/a/test/path'; + + // const expected: EnvironmentDetails = { + // interpreterPath: pythonPath, + // version: ['3', '9', '0'], + // environmentType: [PythonEnvKind.System], + // metadata: { + // sysPrefix: 'prefix/path', + // bitness: Architecture.x64, + // project: Uri.file('path/to/project'), + // }, + // envFolderPath: undefined, + // }; + + // discoverAPI.setup((d) => d.getEnvs()).returns(() => []); + // discoverAPI + // .setup((p) => p.resolveEnv(pythonPath)) + // .returns(() => + // Promise.resolve( + // buildEnvInfo({ + // executable: pythonPath, + // version: { + // major: 3, + // minor: 9, + // micro: 0, + // }, + // kind: PythonEnvKind.System, + // arch: Architecture.x64, + // sysPrefix: 'prefix/path', + // searchLocation: Uri.file('path/to/project'), + // }), + // ), + // ); + + // const actual = await proposed.environment.getEnvironmentDetails(pythonPath, { useCache: false }); + // expect(actual).to.be.deep.equal(expected); + // }); + + // test('getInterpreterDetails: from cache', async () => { + // const pythonPath = 'this/is/a/test/path'; + + // const expected: EnvironmentDetails = { + // interpreterPath: pythonPath, + // version: ['3', '9', '0'], + // environmentType: [PythonEnvKind.System], + // metadata: { + // sysPrefix: 'prefix/path', + // bitness: Architecture.x64, + // project: undefined, + // }, + // envFolderPath: undefined, + // }; + + // discoverAPI + // .setup((d) => d.getEnvs()) + // .returns(() => [ + // { + // executable: { + // filename: pythonPath, + // ctime: 1, + // mtime: 2, + // sysPrefix: 'prefix/path', + // }, + // version: { + // major: 3, + // minor: 9, + // micro: 0, + // }, + // kind: PythonEnvKind.System, + // arch: Architecture.x64, + // name: '', + // location: '', + // source: [PythonEnvSource.PathEnvVar], + // distro: { + // org: '', + // }, + // }, + // ]); + // discoverAPI + // .setup((p) => p.resolveEnv(pythonPath)) + // .returns(() => + // Promise.resolve( + // buildEnvInfo({ + // executable: pythonPath, + // version: { + // major: 3, + // minor: 9, + // micro: 0, + // }, + // kind: PythonEnvKind.System, + // arch: Architecture.x64, + // sysPrefix: 'prefix/path', + // }), + // ), + // ); + + // const actual = await proposed.environment.getEnvironmentDetails(pythonPath, { useCache: true }); + // expect(actual).to.be.deep.equal(expected); + // }); + + // test('getInterpreterDetails: cache miss', async () => { + // const pythonPath = 'this/is/a/test/path'; + + // const expected: EnvironmentDetails = { + // interpreterPath: pythonPath, + // version: ['3', '9', '0'], + // environmentType: [PythonEnvKind.System], + // metadata: { + // sysPrefix: 'prefix/path', + // bitness: Architecture.x64, + // project: undefined, + // }, + // envFolderPath: undefined, + // }; + + // // Force this API to return empty to cause a cache miss. + // discoverAPI.setup((d) => d.getEnvs()).returns(() => []); + // discoverAPI + // .setup((p) => p.resolveEnv(pythonPath)) + // .returns(() => + // Promise.resolve( + // buildEnvInfo({ + // executable: pythonPath, + // version: { + // major: 3, + // minor: 9, + // micro: 0, + // }, + // kind: PythonEnvKind.System, + // arch: Architecture.x64, + // sysPrefix: 'prefix/path', + // }), + // ), + // ); + + // const actual = await proposed.environment.getEnvironmentDetails(pythonPath, { useCache: true }); + // expect(actual).to.be.deep.equal(expected); + // }); + + // test('getInterpreterPaths: no pythons found', async () => { + // discoverAPI.setup((d) => d.getEnvs()).returns(() => []); + // const actual = await proposed.environment.getEnvironmentPaths(); + // expect(actual).to.be.deep.equal([]); + // }); + + // test('getInterpreterPaths: python found', async () => { + // discoverAPI + // .setup((d) => d.getEnvs()) + // .returns(() => [ + // { + // executable: { + // filename: 'this/is/a/test/python/path1', + // ctime: 1, + // mtime: 2, + // sysPrefix: 'prefix/path', + // }, + // version: { + // major: 3, + // minor: 9, + // micro: 0, + // }, + // kind: PythonEnvKind.System, + // arch: Architecture.x64, + // name: '', + // location: '', + // source: [PythonEnvSource.PathEnvVar], + // distro: { + // org: '', + // }, + // }, + // { + // executable: { + // filename: 'this/is/a/test/python/path2', + // ctime: 1, + // mtime: 2, + // sysPrefix: 'prefix/path', + // }, + // version: { + // major: 3, + // minor: 10, + // micro: 0, + // }, + // kind: PythonEnvKind.Venv, + // arch: Architecture.x64, + // name: '', + // location: '', + // source: [PythonEnvSource.PathEnvVar], + // distro: { + // org: '', + // }, + // }, + // ]); + // const actual = await proposed.environment.getEnvironmentPaths(); + // expect(actual?.map((a) => a.path)).to.be.deep.equal([ + // 'this/is/a/test/python/path1', + // 'this/is/a/test/python/path2', + // ]); + // }); + + // test('setActiveInterpreter: no resource', async () => { + // interpreterPathService + // .setup((i) => i.update(undefined, ConfigurationTarget.WorkspaceFolder, 'this/is/a/test/python/path')) + // .returns(() => Promise.resolve()) + // .verifiable(typemoq.Times.once()); + + // await proposed.environment.setActiveEnvironment('this/is/a/test/python/path'); + + // interpreterPathService.verifyAll(); + // }); + // test('setActiveInterpreter: with resource', async () => { + // const resource = Uri.parse('a'); + // interpreterPathService + // .setup((i) => i.update(resource, ConfigurationTarget.WorkspaceFolder, 'this/is/a/test/python/path')) + // .returns(() => Promise.resolve()) + // .verifiable(typemoq.Times.once()); + + // await proposed.environment.setActiveEnvironment('this/is/a/test/python/path', resource); + + // interpreterPathService.verifyAll(); + // }); + + // test('refreshInterpreters: common scenario', async () => { + // discoverAPI + // .setup((d) => d.triggerRefresh(undefined, undefined)) + // .returns(() => Promise.resolve()) + // .verifiable(typemoq.Times.once()); + // discoverAPI + // .setup((d) => d.getEnvs()) + // .returns(() => [ + // { + // executable: { + // filename: 'this/is/a/test/python/path1', + // ctime: 1, + // mtime: 2, + // sysPrefix: 'prefix/path', + // }, + // version: { + // major: 3, + // minor: 9, + // micro: 0, + // }, + // kind: PythonEnvKind.System, + // arch: Architecture.x64, + // name: '', + // location: 'this/is/a/test/python/path1/folder', + // source: [PythonEnvSource.PathEnvVar], + // distro: { + // org: '', + // }, + // }, + // { + // executable: { + // filename: 'this/is/a/test/python/path2', + // ctime: 1, + // mtime: 2, + // sysPrefix: 'prefix/path', + // }, + // version: { + // major: 3, + // minor: 10, + // micro: 0, + // }, + // kind: PythonEnvKind.Venv, + // arch: Architecture.x64, + // name: '', + // location: '', + // source: [PythonEnvSource.PathEnvVar], + // distro: { + // org: '', + // }, + // }, + // ]); + + // const actual = await proposed.environment.refreshEnvironment(); + // expect(actual).to.be.deep.equal([ + // { path: 'this/is/a/test/python/path1/folder', pathType: 'envFolderPath' }, + // { path: 'this/is/a/test/python/path2', pathType: 'interpreterPath' }, + // ]); + // discoverAPI.verifyAll(); + // }); + + // test('getRefreshPromise: common scenario', () => { + // const expected = Promise.resolve(); + // discoverAPI + // .setup((d) => d.getRefreshPromise(typemoq.It.isValue({ stage: ProgressReportStage.allPathsDiscovered }))) + // .returns(() => expected); + // const actual = proposed.environment.getRefreshPromise({ stage: ProgressReportStage.allPathsDiscovered }); + + // // We are comparing instances here, they should be the same instance. + // // So '==' is ok here. + // // eslint-disable-next-line eqeqeq + // expect(actual == expected).is.equal(true); + // }); }); From 803a137ec8989bbc3e835eff39ec8f3cceb3e7c7 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 16 Sep 2022 14:10:37 -0700 Subject: [PATCH 28/60] Be consistent with interface names and types --- src/client/proposedApi.ts | 31 ++++++++---- src/client/proposedApiTypes.ts | 79 ++++++++++++++++++------------- src/test/proposedApi.unit.test.ts | 5 +- 3 files changed, 72 insertions(+), 43 deletions(-) diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index 4dabde2c6392..4f07331d0976 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -4,6 +4,7 @@ import { ConfigurationTarget, EventEmitter, Uri } from 'vscode'; import { IDisposableRegistry, IInterpreterPathService } from './common/types'; +import { Architecture } from './common/utils/platform'; import { IInterpreterService } from './interpreter/contracts'; import { IServiceContainer } from './ioc/types'; import { @@ -12,7 +13,7 @@ import { EnvironmentsChangedParams, IProposedExtensionAPI, ResolvedEnvironment, - UniquePathType, + UniquePath, PythonVersionInfo, RefreshOptions, Resource, @@ -29,7 +30,7 @@ export function reportActiveInterpreterChanged(e: ActiveEnvironmentChangedParams } const onProgress = new EventEmitter(); const onEnvironmentsChanged = new EventEmitter(); -const environmentsReference = new Map(); +const environmentsReference = new Map(); export class EnvironmentReference implements Environment { readonly pathID: string; @@ -76,10 +77,10 @@ export function buildProposedApi( disposables.push( discoveryApi.onProgress((e) => { if (e.stage === ProgressReportStage.discoveryStarted) { - onProgress.fire({ state: RefreshStateValue.started }); + onProgress.fire({ stateValue: RefreshStateValue.started }); } if (e.stage === ProgressReportStage.discoveryFinished) { - onProgress.fire({ state: RefreshStateValue.finished }); + onProgress.fire({ stateValue: RefreshStateValue.finished }); } }), discoveryApi.onChanged((e) => { @@ -128,11 +129,11 @@ export function buildProposedApi( get refreshState(): RefreshState { switch (discoveryApi.refreshState) { case ProgressReportStage.discoveryStarted: - return { state: RefreshStateValue.started }; + return { stateValue: RefreshStateValue.started }; case ProgressReportStage.discoveryFinished: - return { state: RefreshStateValue.finished }; + return { stateValue: RefreshStateValue.finished }; default: - return { state: RefreshStateValue.started }; + return { stateValue: RefreshStateValue.started }; } }, async refreshEnvironments(options: RefreshOptions) { @@ -159,11 +160,11 @@ async function resolveEnvironment(path: string, discoveryApi: IDiscoveryAPI): Pr export function convertCompleteEnvInfo(env: PythonEnvInfo): ResolvedEnvironment { const version = { ...env.version, sysVersion: env.version.sysVersion }; - return { + const resolvedEnv: ResolvedEnvironment = { pathID: getEnvPath(env.executable.filename, env.location).path, executable: { uri: Uri.file(env.executable.filename), - bitness: env.arch, + bitness: convertArch(env.arch), sysPrefix: env.executable.sysPrefix, }, environment: virtualEnvKinds.includes(env.kind) @@ -177,6 +178,7 @@ export function convertCompleteEnvInfo(env: PythonEnvInfo): ResolvedEnvironment : undefined, version: version as PythonVersionInfo, }; + return resolvedEnv; } function convertEnvInfoAndGetReference(env: PythonEnvInfo): Environment { @@ -201,3 +203,14 @@ function convertEnvInfoAndGetReference(env: PythonEnvInfo): Environment { } return getEnvReference(convertedEnv); } + +function convertArch(arch: Architecture) { + switch (arch) { + case Architecture.x64: + return 'x64'; + case Architecture.x86: + return 'x86'; + default: + return 'Unknown'; + } +} diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index 9fe56e4102e3..708678add776 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -23,19 +23,15 @@ interface IEnvironmentAPI { * @param environment : Environment whose details you need. Can also pass the full path to environment folder * or python executable for the environment. */ - resolveEnvironment(environment: Environment | UniquePathType): Promise; + resolveEnvironment(environment: Environment | UniquePath): Promise; } interface IActiveEnvironmentAPI { /** - * This event is triggered when the active environment changes. - */ - onDidChange: Event; - /** - * Returns the environment selected. 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. - * Uses the cache by default, otherwise fetches full information about the environment. - * @param resource : Uri of a file or workspace folder. + * Returns the environment selected. Uses the cache by default, otherwise fetches full information about the + * environment. + * @param resource : Uri of a file or workspace folder. This is used to determine the env in a multi-root + * scenario. If `undefined`, then the API returns what ever is set for the workspace. */ fetch(resource?: Resource): Promise; /** @@ -45,7 +41,11 @@ interface IActiveEnvironmentAPI { * the environment itself. * @param resource : [optional] File or workspace to scope to a particular workspace folder. */ - update(environment: Environment | UniquePathType, resource?: Resource): Promise; + update(environment: Environment | UniquePath, resource?: Resource): Promise; + /** + * This event is triggered when the active environment changes. + */ + onDidChange: Event; } interface IEnvironmentLocatorAPI { @@ -84,8 +84,11 @@ interface IEnvironmentLocatorAPI { /** * Details about the environment. Note the environment folder, type and name never changes over time. */ -export interface Environment { - pathID: UniquePathType; +export type Environment = { + /** + * See {@link UniquePath} for description. + */ + pathID: UniquePath; /** * Carries details about python executable. */ @@ -141,29 +144,30 @@ export interface Environment { */ sysVersion: string | undefined; }; -} +}; -type MakeNonNullable = Omit & NonNullable>; type MakeAllPropertiesNonNullable = { [P in keyof T]: NonNullable; }; +type MakeNonNullable = Omit & MakeAllPropertiesNonNullable>; type ExecutableInfo = MakeNonNullable & MakeNonNullable; + type EnvironmentInfo = NonNullable['environment']>; export type PythonVersionInfo = MakeAllPropertiesNonNullable; /** - * Derived form of {@link Environment} with complete information. + * Derived form of {@link Environment} with complete information, which means certain properties can no longer be `undefined`. */ export interface ResolvedEnvironment { - pathID: UniquePathType; + pathID: UniquePath; executable: ExecutableInfo; environment: EnvironmentInfo | undefined; version: PythonVersionInfo; } export type RefreshState = { - state: RefreshStateValue; + stateValue: RefreshStateValue; }; /** @@ -193,7 +197,7 @@ export type Resource = Uri | WorkspaceFolder; * 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 type UniquePath = string; export type EnvironmentsChangedParams = { env: Environment; @@ -205,13 +209,16 @@ export type EnvironmentsChangedParams = { type: 'add' | 'remove' | 'update'; }; -export interface ActiveEnvironmentChangedParams { - pathID: UniquePathType; +export type ActiveEnvironmentChangedParams = { + /** + * See {@link UniquePath} for description. + */ + pathID: UniquePath; /** * Workspace folder the environment changed for. */ resource: WorkspaceFolder | undefined; -} +}; export type RefreshOptions = { /** @@ -223,27 +230,33 @@ export type RefreshOptions = { ifNotRefreshedAlready: boolean | undefined; }; +/** + * Tool/plugin where the environment came from. It could be {@link KnownEnvSources} or custom string which was + * contributed. + */ export type EnvSource = KnownEnvSources | string; +/** + * Tools or plugins the Python extension is aware of. + */ export type KnownEnvSources = 'Conda' | 'Pipenv' | 'Poetry' | 'VirtualEnv' | 'Venv' | 'VirtualEnvWrapper' | 'Pyenv'; +/** + * Type of the environment. It could be {@link KnownEnvTypes} or custom string which was contributed. + */ export type EnvType = KnownEnvTypes | string; +/** + * Environment types the Python extension is aware of. + */ export type KnownEnvTypes = 'VirtualEnv' | 'Conda' | 'Unknown'; -export enum Architecture { - Unknown = 1, - x86 = 2, - x64 = 3, -} - +/** + * Carries bitness for an environment. + */ +export type Architecture = 'x86' | 'x64' | 'Unknown'; /** * The possible Python release levels. */ -export enum PythonReleaseLevel { - Alpha = 'alpha', - Beta = 'beta', - Candidate = 'candidate', - Final = 'final', -} +export type PythonReleaseLevel = 'alpha' | 'beta' | 'candidate' | 'final'; /** * Release information for a Python version. diff --git a/src/test/proposedApi.unit.test.ts b/src/test/proposedApi.unit.test.ts index 09062faa7783..a2479ddc8662 100644 --- a/src/test/proposedApi.unit.test.ts +++ b/src/test/proposedApi.unit.test.ts @@ -66,7 +66,10 @@ suite('Proposed Extension API', () => { onDidChangeRefreshState.fire({ stage: ProgressReportStage.discoveryFinished }); await sleep(1); - assert.deepEqual(events, [{ state: RefreshStateValue.started }, { state: RefreshStateValue.finished }]); + assert.deepEqual(events, [ + { stateValue: RefreshStateValue.started }, + { stateValue: RefreshStateValue.finished }, + ]); }); test('Provide an event to track when active environment details change', async () => { From 62e2ce664f51a9604dad31e2ded6ce3cc77c0fb5 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 16 Sep 2022 15:09:25 -0700 Subject: [PATCH 29/60] Clarify using NonNullable --- src/client/extension.ts | 4 +- src/client/proposedApi.ts | 16 +++--- src/client/proposedApiTypes.ts | 88 ++++++++++++++++++++----------- src/test/proposedApi.unit.test.ts | 8 +-- 4 files changed, 72 insertions(+), 44 deletions(-) diff --git a/src/client/extension.ts b/src/client/extension.ts index 30da91a0d2ba..9c59a72c24be 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -46,7 +46,7 @@ import { IExtensionApi } from './apiTypes'; import { buildProposedApi } from './proposedApi'; import { WorkspaceService } from './common/application/workspace'; import { disposeAll } from './common/utils/resourceLifecycle'; -import { IProposedExtensionAPI } from './proposedApiTypes'; +import { ProposedExtensionAPI } from './proposedApiTypes'; durations.codeLoadingTime = stopWatch.elapsedTime; @@ -104,7 +104,7 @@ async function activateUnsafe( context: IExtensionContext, startupStopWatch: StopWatch, startupDurations: IStartupDurations, -): Promise<[IExtensionApi & IProposedExtensionAPI, Promise, IServiceContainer]> { +): Promise<[IExtensionApi & ProposedExtensionAPI, Promise, IServiceContainer]> { // Add anything that we got from initializing logs to dispose. context.subscriptions.push(...logDispose); diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index 4f07331d0976..eaeed4cb9f9c 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -8,10 +8,10 @@ import { Architecture } from './common/utils/platform'; import { IInterpreterService } from './interpreter/contracts'; import { IServiceContainer } from './ioc/types'; import { - ActiveEnvironmentChangedParams, + ActiveEnvironmentChangeEvent, Environment, - EnvironmentsChangedParams, - IProposedExtensionAPI, + EnvironmentsChangedEvent, + ProposedExtensionAPI, ResolvedEnvironment, UniquePath, PythonVersionInfo, @@ -24,12 +24,12 @@ import { PythonEnvInfo, PythonEnvKind, virtualEnvKinds } from './pythonEnvironme import { getEnvPath } from './pythonEnvironments/base/info/env'; import { IDiscoveryAPI, ProgressReportStage } from './pythonEnvironments/base/locator'; -const onDidActiveInterpreterChangedEvent = new EventEmitter(); -export function reportActiveInterpreterChanged(e: ActiveEnvironmentChangedParams): void { +const onDidActiveInterpreterChangedEvent = new EventEmitter(); +export function reportActiveInterpreterChanged(e: ActiveEnvironmentChangeEvent): void { onDidActiveInterpreterChangedEvent.fire(e); } const onProgress = new EventEmitter(); -const onEnvironmentsChanged = new EventEmitter(); +const onEnvironmentsChanged = new EventEmitter(); const environmentsReference = new Map(); export class EnvironmentReference implements Environment { @@ -70,7 +70,7 @@ function getEnvReference(e: Environment) { export function buildProposedApi( discoveryApi: IDiscoveryAPI, serviceContainer: IServiceContainer, -): IProposedExtensionAPI { +): ProposedExtensionAPI { const interpreterPathService = serviceContainer.get(IInterpreterPathService); const interpreterService = serviceContainer.get(IInterpreterService); const disposables = serviceContainer.get(IDisposableRegistry); @@ -97,7 +97,7 @@ export function buildProposedApi( onProgress, onEnvironmentsChanged, ); - const proposed: IProposedExtensionAPI = { + const proposed: ProposedExtensionAPI = { environment: { activeEnvironment: { async fetch(resource?: Resource) { diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index 708678add776..4cbba41ab15e 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -5,19 +5,19 @@ import { CancellationToken, Event, Uri, WorkspaceFolder } from 'vscode'; // https://github.com/microsoft/vscode-python/wiki/Proposed-Environment-APIs -export interface IProposedExtensionAPI { - environment: IEnvironmentAPI; +export interface ProposedExtensionAPI { + environment: EnvironmentAPI; } -interface IEnvironmentAPI { +interface EnvironmentAPI { /** * Carries the API to track the selected environment by the user for a workspace. */ - activeEnvironment: IActiveEnvironmentAPI; + activeEnvironment: ActiveEnvironmentAPI; /** * Carries the API necessary for locating environments. */ - locator: IEnvironmentLocatorAPI; + locator: EnvironmentLocatorAPI; /** * Returns details for the given environment, or `undefined` if the env is invalid. * @param environment : Environment whose details you need. Can also pass the full path to environment folder @@ -26,7 +26,7 @@ interface IEnvironmentAPI { resolveEnvironment(environment: Environment | UniquePath): Promise; } -interface IActiveEnvironmentAPI { +interface ActiveEnvironmentAPI { /** * Returns the environment selected. Uses the cache by default, otherwise fetches full information about the * environment. @@ -45,10 +45,10 @@ interface IActiveEnvironmentAPI { /** * This event is triggered when the active environment changes. */ - onDidChange: Event; + onDidChange: Event; } -interface IEnvironmentLocatorAPI { +interface EnvironmentLocatorAPI { /** * Carries environments found by the extension at the time of fetching the property. Note a refresh might be * going on so this may not be the complete list. To wait on complete list use {@link refreshState()} and @@ -59,14 +59,14 @@ interface IEnvironmentLocatorAPI { * 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; + onDidChangeEnvironments: Event; /** - * Returns the last known state in the refresh, i.e whether it started, finished, or any other relevant state. + * Carries the current state in the refresh, i.e whether it started, finished, or any other relevant state. */ refreshState: RefreshState; /** - * Tracks refresh progress for current list of known environments, i.e when it starts, finishes or any other - * relevant state. + * Fires when a refresh state has been reached, i.e when it starts, finishes or any other relevant state. + * Tracks refresh progress for current list of known environments. */ readonly onDidChangeRefreshState: Event; /** @@ -115,7 +115,7 @@ export type Environment = { /** * Type of the environment. */ - type: EnvType; + type: EnvironmentType; /** * Name to the environment if any. */ @@ -132,7 +132,7 @@ export type Environment = { * Tools/plugins which created the environment or where it came from. First value in array corresponds * to the primary source, which never changes over time. */ - source: EnvSource[]; + source: EnvironmentSource[]; } | undefined; /** @@ -146,23 +146,43 @@ export type Environment = { }; }; +/** + * A new form of object `T` where no property can have the value of `undefined`. + */ type MakeAllPropertiesNonNullable = { [P in keyof T]: NonNullable; }; -type MakeNonNullable = Omit & MakeAllPropertiesNonNullable>; -type ExecutableInfo = MakeNonNullable & - MakeNonNullable; +/** + * A new form of object `Type` where a property represented by `Key` cannot be `undefined`. + */ +type MakePropertyNonNullable = Omit & + MakeAllPropertiesNonNullable>; -type EnvironmentInfo = NonNullable['environment']>; +type ExecutableInfo = MakePropertyNonNullable & + MakePropertyNonNullable; export type PythonVersionInfo = MakeAllPropertiesNonNullable; /** - * Derived form of {@link Environment} with complete information, which means certain properties can no longer be `undefined`. + * Derived form of {@link Environment} where certain properties can no longer be `undefined`. Meant to represent an + * {@link Environment} with complete information. */ export interface ResolvedEnvironment { + /** + * See {@link UniquePath} for description. + */ pathID: UniquePath; + /** + * New form of {@link Environment.executable} object where properties `sysPrefix` and `bitness` cannot be + * `undefined`. + */ executable: ExecutableInfo; - environment: EnvironmentInfo | undefined; + /** + * See {@link Environment.environment} for description. + */ + environment: Environment['environment']; + /** + * New form of {@link Environment.version} object where no properties can be `undefined`. + */ version: PythonVersionInfo; } @@ -171,7 +191,7 @@ export type RefreshState = { }; /** - * Value of the enum indicates which states comes before when a refresh takes place. + * Contains state values in the order they finish during a refresh cycle. */ export enum RefreshStateValue { /** @@ -199,7 +219,7 @@ export type Resource = Uri | WorkspaceFolder; */ export type UniquePath = string; -export type EnvironmentsChangedParams = { +export type EnvironmentsChangedEvent = { env: Environment; /** * * "add": New environment is added. @@ -209,7 +229,7 @@ export type EnvironmentsChangedParams = { type: 'add' | 'remove' | 'update'; }; -export type ActiveEnvironmentChangedParams = { +export type ActiveEnvironmentChangeEvent = { /** * See {@link UniquePath} for description. */ @@ -231,28 +251,36 @@ export type RefreshOptions = { }; /** - * Tool/plugin where the environment came from. It could be {@link KnownEnvSources} or custom string which was - * contributed. + * Tool/plugin where the environment came from. It can be {@link KnownEnvironmentSources} or custom string which + * was contributed. */ -export type EnvSource = KnownEnvSources | string; +export type EnvironmentSource = KnownEnvironmentSources | string; /** * Tools or plugins the Python extension is aware of. */ -export type KnownEnvSources = 'Conda' | 'Pipenv' | 'Poetry' | 'VirtualEnv' | 'Venv' | 'VirtualEnvWrapper' | 'Pyenv'; +export type KnownEnvironmentSources = + | 'Conda' + | 'Pipenv' + | 'Poetry' + | 'VirtualEnv' + | 'Venv' + | 'VirtualEnvWrapper' + | 'Pyenv'; /** - * Type of the environment. It could be {@link KnownEnvTypes} or custom string which was contributed. + * Type of the environment. It can be {@link KnownEnvironmentTypes} or custom string which was contributed. */ -export type EnvType = KnownEnvTypes | string; +export type EnvironmentType = KnownEnvironmentTypes | string; /** * Environment types the Python extension is aware of. */ -export type KnownEnvTypes = 'VirtualEnv' | 'Conda' | 'Unknown'; +export type KnownEnvironmentTypes = 'VirtualEnv' | 'Conda' | 'Unknown'; /** * Carries bitness for an environment. */ export type Architecture = 'x86' | 'x64' | 'Unknown'; + /** * The possible Python release levels. */ diff --git a/src/test/proposedApi.unit.test.ts b/src/test/proposedApi.unit.test.ts index a2479ddc8662..1f50532dbdac 100644 --- a/src/test/proposedApi.unit.test.ts +++ b/src/test/proposedApi.unit.test.ts @@ -21,8 +21,8 @@ import { import { PythonEnvironment } from '../client/pythonEnvironments/info'; import { buildEnvInfo } from '../client/pythonEnvironments/base/info/env'; import { - ActiveEnvironmentChangedParams, - IProposedExtensionAPI, + ActiveEnvironmentChangeEvent, + ProposedExtensionAPI, RefreshState, RefreshStateValue, } from '../client/proposedApiTypes'; @@ -36,7 +36,7 @@ suite('Proposed Extension API', () => { let onDidExecutionEvent: Event; let onDidChangeRefreshState: EventEmitter; - let proposed: IProposedExtensionAPI; + let proposed: ProposedExtensionAPI; setup(() => { serviceContainer = typemoq.Mock.ofType(); @@ -73,7 +73,7 @@ suite('Proposed Extension API', () => { }); test('Provide an event to track when active environment details change', async () => { - const events: ActiveEnvironmentChangedParams[] = []; + const events: ActiveEnvironmentChangeEvent[] = []; proposed.environment.activeEnvironment.onDidChange((e) => { events.push(e); }); From ef4be8f3f3b8ee30f991012cf90950fefef94e4c Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 19 Sep 2022 16:21:54 -0700 Subject: [PATCH 30/60] Use env type to figure out type information --- src/client/proposedApi.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index eaeed4cb9f9c..fe1b31c542fe 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -19,8 +19,9 @@ import { Resource, RefreshStateValue, RefreshState, + EnvironmentType, } from './proposedApiTypes'; -import { PythonEnvInfo, PythonEnvKind, virtualEnvKinds } from './pythonEnvironments/base/info'; +import { PythonEnvInfo, PythonEnvKind, PythonEnvType, virtualEnvKinds } from './pythonEnvironments/base/info'; import { getEnvPath } from './pythonEnvironments/base/info/env'; import { IDiscoveryAPI, ProgressReportStage } from './pythonEnvironments/base/locator'; @@ -167,9 +168,9 @@ export function convertCompleteEnvInfo(env: PythonEnvInfo): ResolvedEnvironment bitness: convertArch(env.arch), sysPrefix: env.executable.sysPrefix, }, - environment: virtualEnvKinds.includes(env.kind) + environment: env.type ? { - type: env.kind === PythonEnvKind.Conda ? 'Conda' : 'VirtualEnv', + type: convertEnvType(env.type), name: env.name, folderUri: Uri.file(env.location), workspaceFolder: env.searchLocation, @@ -181,6 +182,16 @@ export function convertCompleteEnvInfo(env: PythonEnvInfo): ResolvedEnvironment return resolvedEnv; } +function convertEnvType(envType: PythonEnvType): EnvironmentType { + if (envType === PythonEnvType.Conda) { + return 'Conda'; + } + if (envType === PythonEnvType.Virtual) { + return 'VirtualEnv'; + } + return 'Unknown'; +} + function convertEnvInfoAndGetReference(env: PythonEnvInfo): Environment { const convertedEnv = convertCompleteEnvInfo(env) as Environment; if (convertedEnv.executable.sysPrefix === '') { From ae1c81c53d3e433249af0126e1562c2d1324addf Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 19 Sep 2022 16:30:23 -0700 Subject: [PATCH 31/60] Try kind to infer the source of the environment --- src/client/proposedApi.ts | 26 ++++++++++++++++++++++++-- src/client/proposedApiTypes.ts | 5 +++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index fe1b31c542fe..1c7a88204010 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -20,8 +20,9 @@ import { RefreshStateValue, RefreshState, EnvironmentType, + EnvironmentSource, } from './proposedApiTypes'; -import { PythonEnvInfo, PythonEnvKind, PythonEnvType, virtualEnvKinds } from './pythonEnvironments/base/info'; +import { PythonEnvInfo, PythonEnvKind, PythonEnvType } from './pythonEnvironments/base/info'; import { getEnvPath } from './pythonEnvironments/base/info/env'; import { IDiscoveryAPI, ProgressReportStage } from './pythonEnvironments/base/locator'; @@ -174,7 +175,7 @@ export function convertCompleteEnvInfo(env: PythonEnvInfo): ResolvedEnvironment name: env.name, folderUri: Uri.file(env.location), workspaceFolder: env.searchLocation, - source: [env.kind], + source: [convertKind(env.kind)], } : undefined, version: version as PythonVersionInfo, @@ -192,6 +193,27 @@ function convertEnvType(envType: PythonEnvType): EnvironmentType { return 'Unknown'; } +function convertKind(kind: PythonEnvKind): EnvironmentSource { + switch (kind) { + case PythonEnvKind.Venv: + return 'Venv'; + case PythonEnvKind.Pipenv: + return 'Pipenv'; + case PythonEnvKind.Poetry: + return 'Poetry'; + case PythonEnvKind.VirtualEnvWrapper: + return 'VirtualEnvWrapper'; + case PythonEnvKind.VirtualEnv: + return 'VirtualEnv'; + case PythonEnvKind.Conda: + return 'Conda'; + case PythonEnvKind.Pyenv: + return 'Pyenv'; + default: + return 'Unknown'; + } +} + function convertEnvInfoAndGetReference(env: PythonEnvInfo): Environment { const convertedEnv = convertCompleteEnvInfo(env) as Environment; if (convertedEnv.executable.sysPrefix === '') { diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index 4cbba41ab15e..0c373d6359d1 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -254,7 +254,7 @@ export type RefreshOptions = { * Tool/plugin where the environment came from. It can be {@link KnownEnvironmentSources} or custom string which * was contributed. */ -export type EnvironmentSource = KnownEnvironmentSources | string; +export type EnvironmentSource = KnownEnvironmentSources; /** * Tools or plugins the Python extension is aware of. */ @@ -265,7 +265,8 @@ export type KnownEnvironmentSources = | 'VirtualEnv' | 'Venv' | 'VirtualEnvWrapper' - | 'Pyenv'; + | 'Pyenv' + | 'Unknown'; /** * Type of the environment. It can be {@link KnownEnvironmentTypes} or custom string which was contributed. From ebaae15e56eb2ea9782f5bb2f1651a2ae377794a Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 20 Sep 2022 08:51:41 -0700 Subject: [PATCH 32/60] Flatten the APIs out under the environment umbrella --- src/client/proposedApi.ts | 76 +++++++++++++++---------------- src/client/proposedApiTypes.ts | 32 ++++--------- src/test/proposedApi.unit.test.ts | 6 +-- 3 files changed, 48 insertions(+), 66 deletions(-) diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index 1c7a88204010..f738ffd08453 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -101,51 +101,47 @@ export function buildProposedApi( ); const proposed: ProposedExtensionAPI = { environment: { - activeEnvironment: { - async fetch(resource?: Resource) { - resource = resource && 'uri' in resource ? resource.uri : resource; - const env = await interpreterService.getActiveInterpreter(resource); - if (!env) { - return undefined; - } - return resolveEnvironment(getEnvPath(env.path, env.envPath).path, discoveryApi); - }, - update(env: string | Environment, resource?: Resource): Promise { - const path = typeof env !== 'string' ? env.pathID : env; - resource = resource && 'uri' in resource ? resource.uri : resource; - return interpreterPathService.update(resource, ConfigurationTarget.WorkspaceFolder, path); - }, - onDidChange: onDidActiveInterpreterChangedEvent.event, + async fetchActiveEnvironment(resource?: Resource) { + resource = resource && 'uri' in resource ? resource.uri : resource; + const env = await interpreterService.getActiveInterpreter(resource); + if (!env) { + return undefined; + } + return resolveEnvironment(getEnvPath(env.path, env.envPath).path, discoveryApi); + }, + updateActiveEnvironment(env: string | Environment, resource?: Resource): Promise { + const path = typeof env !== 'string' ? env.pathID : env; + resource = resource && 'uri' in resource ? resource.uri : resource; + return interpreterPathService.update(resource, ConfigurationTarget.WorkspaceFolder, path); }, + onDidChangeActiveEnvironment: onDidActiveInterpreterChangedEvent.event, resolveEnvironment: (env: string | Environment) => { const path = typeof env !== 'string' ? env.pathID : env; return resolveEnvironment(path, discoveryApi); }, - locator: { - get environments(): Environment[] { - return discoveryApi.getEnvs().map((e) => convertEnvInfoAndGetReference(e)); - }, - get onDidChangeRefreshState() { - return onProgress.event; - }, - get refreshState(): RefreshState { - switch (discoveryApi.refreshState) { - case ProgressReportStage.discoveryStarted: - return { stateValue: RefreshStateValue.started }; - case ProgressReportStage.discoveryFinished: - return { stateValue: RefreshStateValue.finished }; - default: - return { stateValue: RefreshStateValue.started }; - } - }, - async refreshEnvironments(options: RefreshOptions) { - await discoveryApi.triggerRefresh(undefined, { - ifNotTriggerredAlready: options.ifNotRefreshedAlready, - }); - }, - get onDidChangeEnvironments() { - return onEnvironmentsChanged.event; - }, + get environments(): Environment[] { + return discoveryApi.getEnvs().map((e) => convertEnvInfoAndGetReference(e)); + }, + get onDidChangeRefreshState() { + return onProgress.event; + }, + get refreshState(): RefreshState { + switch (discoveryApi.refreshState) { + case ProgressReportStage.discoveryStarted: + return { stateValue: RefreshStateValue.started }; + case ProgressReportStage.discoveryFinished: + return { stateValue: RefreshStateValue.finished }; + default: + return { stateValue: RefreshStateValue.started }; + } + }, + async refreshEnvironments(options: RefreshOptions) { + await discoveryApi.triggerRefresh(undefined, { + ifNotTriggerredAlready: options.ifNotRefreshedAlready, + }); + }, + get onDidChangeEnvironments() { + return onEnvironmentsChanged.event; }, }, }; diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index 0c373d6359d1..5e466ff8f18d 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -10,30 +10,13 @@ export interface ProposedExtensionAPI { } interface EnvironmentAPI { - /** - * Carries the API to track the selected environment by the user for a workspace. - */ - activeEnvironment: ActiveEnvironmentAPI; - /** - * Carries the API necessary for locating environments. - */ - locator: EnvironmentLocatorAPI; - /** - * Returns details for the given environment, or `undefined` if the env is invalid. - * @param environment : Environment whose details you need. Can also pass the full path to environment folder - * or python executable for the environment. - */ - resolveEnvironment(environment: Environment | UniquePath): Promise; -} - -interface ActiveEnvironmentAPI { /** * Returns the environment selected. Uses the cache by default, otherwise fetches full information about the * environment. * @param resource : Uri of a file or workspace folder. This is used to determine the env in a multi-root * scenario. If `undefined`, then the API returns what ever is set for the workspace. */ - fetch(resource?: Resource): Promise; + fetchActiveEnvironment(resource?: Resource): Promise; /** * Sets the active environment path for the python extension for the resource. Configuration target will always * be the workspace folder. @@ -41,14 +24,11 @@ interface ActiveEnvironmentAPI { * the environment itself. * @param resource : [optional] File or workspace to scope to a particular workspace folder. */ - update(environment: Environment | UniquePath, resource?: Resource): Promise; + updateActiveEnvironment(environment: Environment | UniquePath, resource?: Resource): Promise; /** * This event is triggered when the active environment changes. */ - onDidChange: Event; -} - -interface EnvironmentLocatorAPI { + onDidChangeActiveEnvironment: Event; /** * Carries environments found by the extension at the time of fetching the property. Note a refresh might be * going on so this may not be the complete list. To wait on complete list use {@link refreshState()} and @@ -79,6 +59,12 @@ interface EnvironmentLocatorAPI { * @param token A cancellation token that indicates a refresh is no longer needed. */ refreshEnvironments(options: RefreshOptions, token?: CancellationToken): Promise; + /** + * Returns details for the given environment, or `undefined` if the env is invalid. + * @param environment : Environment whose details you need. Can also pass the full path to environment folder + * or python executable for the environment. + */ + resolveEnvironment(environment: Environment | UniquePath): Promise; } /** diff --git a/src/test/proposedApi.unit.test.ts b/src/test/proposedApi.unit.test.ts index 1f50532dbdac..c6875deaa484 100644 --- a/src/test/proposedApi.unit.test.ts +++ b/src/test/proposedApi.unit.test.ts @@ -58,7 +58,7 @@ suite('Proposed Extension API', () => { test('Provides a event for tracking refresh progress', async () => { const events: RefreshState[] = []; - proposed.environment.locator.onDidChangeRefreshState((e) => { + proposed.environment.onDidChangeRefreshState((e) => { events.push(e); }); onDidChangeRefreshState.fire({ stage: ProgressReportStage.discoveryStarted }); @@ -74,7 +74,7 @@ suite('Proposed Extension API', () => { test('Provide an event to track when active environment details change', async () => { const events: ActiveEnvironmentChangeEvent[] = []; - proposed.environment.activeEnvironment.onDidChange((e) => { + proposed.environment.onDidChangeActiveEnvironment((e) => { events.push(e); }); reportActiveInterpreterChanged({ pathID: 'path/to/environment', resource: undefined }); @@ -89,7 +89,7 @@ suite('Proposed Extension API', () => { .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); const env = buildEnvInfo({ executable: pythonPath }); discoverAPI.setup((d) => d.resolveEnv(pythonPath)).returns(() => Promise.resolve(env)); - const actual = await proposed.environment.activeEnvironment.fetch(); + const actual = await proposed.environment.fetchActiveEnvironment(); assert.deepEqual((actual as EnvironmentReference).internal, convertCompleteEnvInfo(env)); }); From ae525bde1a05bfd501e9bd4e9e605da282d95fb7 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 20 Sep 2022 09:52:26 -0700 Subject: [PATCH 33/60] Modify refresh options --- src/client/proposedApi.ts | 2 +- src/client/proposedApiTypes.ts | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index f738ffd08453..01a6d2a5f5b7 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -137,7 +137,7 @@ export function buildProposedApi( }, async refreshEnvironments(options: RefreshOptions) { await discoveryApi.triggerRefresh(undefined, { - ifNotTriggerredAlready: options.ifNotRefreshedAlready, + ifNotTriggerredAlready: !options.forceRefresh, }); }, get onDidChangeEnvironments() { diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index 5e466ff8f18d..4f6950b2dba2 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -50,15 +50,17 @@ interface EnvironmentAPI { */ readonly onDidChangeRefreshState: Event; /** - * This API will re-trigger environment discovery. If there is a refresh already going on then it returns the - * promise for that refresh. + * This API will trigger environment discovery, but only if it has not already happened in this VSCode session. + * Useful for making sure env list is up-to-date when the caller needs it for the first time. * - * Note this can be expensive so it's best to only use it if user manually triggers it. For internal automatic - * triggers consider using {@link RefreshOptions.ifNotRefreshedAlready}. + * To force trigger a refresh regardless of whether a refresh was already triggered, see option + * {@link RefreshOptions.forceRefresh}. + * + * Note that if there is a refresh already going on then this returns the promise for that refresh. * @param options Additional options for refresh. * @param token A cancellation token that indicates a refresh is no longer needed. */ - refreshEnvironments(options: RefreshOptions, token?: CancellationToken): Promise; + refreshEnvironments(options?: RefreshOptions, token?: CancellationToken): Promise; /** * Returns details for the given environment, or `undefined` if the env is invalid. * @param environment : Environment whose details you need. Can also pass the full path to environment folder @@ -228,12 +230,10 @@ export type ActiveEnvironmentChangeEvent = { export type RefreshOptions = { /** - * Only trigger a refresh if not triggered already for this session. Useful for making sure env list is - * up-to-date when extension starts up. - * - * After that users can use extension specific UI to refresh environments when needed. + * Force trigger a refresh regardless of whether a refresh was already triggered. Note this can be expensive so + * it's best to only use it if user manually triggers a refresh. */ - ifNotRefreshedAlready: boolean | undefined; + forceRefresh?: boolean; }; /** From 62d8d03fb696aa22b9991140315542bf89168d9f Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 20 Sep 2022 10:13:27 -0700 Subject: [PATCH 34/60] Add deprecated type --- src/client/proposedApi.ts | 8 ++++++++ src/client/proposedApiTypes.ts | 12 ++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index 01a6d2a5f5b7..fb136b826a0e 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -143,6 +143,14 @@ export function buildProposedApi( get onDidChangeEnvironments() { return onEnvironmentsChanged.event; }, + async getActiveEnvironmentPath(resource?: Resource) { + resource = resource && 'uri' in resource ? resource.uri : resource; + const env = await interpreterService.getActiveInterpreter(resource); + if (!env) { + return undefined; + } + return getEnvPath(env.path, env.envPath); + }, }, }; return proposed; diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index 4f6950b2dba2..7ebc5aaedfbc 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -67,6 +67,10 @@ interface EnvironmentAPI { * or python executable for the environment. */ resolveEnvironment(environment: Environment | UniquePath): Promise; + /** + * @deprecated Use {@link fetchActiveEnvironment} instead. + */ + getActiveEnvironmentPath(resource?: Resource): Promise; } /** @@ -287,3 +291,11 @@ export type StandardVersionInfo = { micro: number | undefined; release: PythonVersionRelease | undefined; }; + +/** + * @deprecated: Will be removed soon. + */ +interface EnvPathType { + path: string; + pathType: 'envFolderPath' | 'interpreterPath'; +} From 101f1601bf874c0473947354eab73acb6d5b158a Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 20 Sep 2022 10:38:01 -0700 Subject: [PATCH 35/60] Rename source to tools and pluck it out of the `environment` umbrella --- src/client/proposedApi.ts | 16 ++++++++++++---- src/client/proposedApiTypes.ts | 20 ++++++++++++-------- src/test/proposedApi.unit.test.ts | 4 ++-- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index fb136b826a0e..a4693a215c3f 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -20,7 +20,7 @@ import { RefreshStateValue, RefreshState, EnvironmentType, - EnvironmentSource, + EnvironmentTools, } from './proposedApiTypes'; import { PythonEnvInfo, PythonEnvKind, PythonEnvType } from './pythonEnvironments/base/info'; import { getEnvPath } from './pythonEnvironments/base/info/env'; @@ -53,6 +53,10 @@ export class EnvironmentReference implements Environment { return this.internal.version; } + get tools() { + return this.internal.tools; + } + updateEnv(newInternal: Environment) { this.internal = newInternal; } @@ -166,6 +170,10 @@ async function resolveEnvironment(path: string, discoveryApi: IDiscoveryAPI): Pr export function convertCompleteEnvInfo(env: PythonEnvInfo): ResolvedEnvironment { const version = { ...env.version, sysVersion: env.version.sysVersion }; + let tool = convertKind(env.kind); + if (env.type && !tool) { + tool = 'Unknown'; + } const resolvedEnv: ResolvedEnvironment = { pathID: getEnvPath(env.executable.filename, env.location).path, executable: { @@ -179,10 +187,10 @@ export function convertCompleteEnvInfo(env: PythonEnvInfo): ResolvedEnvironment name: env.name, folderUri: Uri.file(env.location), workspaceFolder: env.searchLocation, - source: [convertKind(env.kind)], } : undefined, version: version as PythonVersionInfo, + tools: tool ? [tool] : undefined, }; return resolvedEnv; } @@ -197,7 +205,7 @@ function convertEnvType(envType: PythonEnvType): EnvironmentType { return 'Unknown'; } -function convertKind(kind: PythonEnvKind): EnvironmentSource { +function convertKind(kind: PythonEnvKind): EnvironmentTools | undefined { switch (kind) { case PythonEnvKind.Venv: return 'Venv'; @@ -214,7 +222,7 @@ function convertKind(kind: PythonEnvKind): EnvironmentSource { case PythonEnvKind.Pyenv: return 'Pyenv'; default: - return 'Unknown'; + return undefined; } } diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index 7ebc5aaedfbc..2f7b547e80a6 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -120,11 +120,6 @@ export type Environment = { * Any specific workspace folder this environment is created for. */ workspaceFolder: Uri | undefined; - /** - * Tools/plugins which created the environment or where it came from. First value in array corresponds - * to the primary source, which never changes over time. - */ - source: EnvironmentSource[]; } | undefined; /** @@ -136,6 +131,11 @@ export type Environment = { */ sysVersion: string | undefined; }; + /** + * Tools/plugins which created the environment or where it came from. First value in array corresponds + * to the primary tool responsible for the environment, which never changes over time. + */ + tools: EnvironmentTools[] | undefined; }; /** @@ -176,6 +176,10 @@ export interface ResolvedEnvironment { * New form of {@link Environment.version} object where no properties can be `undefined`. */ version: PythonVersionInfo; + /** + * See {@link Environment.tools} for description. + */ + tools: EnvironmentTools[] | undefined; } export type RefreshState = { @@ -241,14 +245,14 @@ export type RefreshOptions = { }; /** - * Tool/plugin where the environment came from. It can be {@link KnownEnvironmentSources} or custom string which + * Tool/plugin where the environment came from. It can be {@link KnownEnvironmentTools} or custom string which * was contributed. */ -export type EnvironmentSource = KnownEnvironmentSources; +export type EnvironmentTools = KnownEnvironmentTools | string; /** * Tools or plugins the Python extension is aware of. */ -export type KnownEnvironmentSources = +export type KnownEnvironmentTools = | 'Conda' | 'Pipenv' | 'Poetry' diff --git a/src/test/proposedApi.unit.test.ts b/src/test/proposedApi.unit.test.ts index c6875deaa484..201c52625c5f 100644 --- a/src/test/proposedApi.unit.test.ts +++ b/src/test/proposedApi.unit.test.ts @@ -20,13 +20,13 @@ import { } from '../client/pythonEnvironments/base/locator'; import { PythonEnvironment } from '../client/pythonEnvironments/info'; import { buildEnvInfo } from '../client/pythonEnvironments/base/info/env'; +import { sleep } from './core'; import { - ActiveEnvironmentChangeEvent, ProposedExtensionAPI, RefreshState, RefreshStateValue, + ActiveEnvironmentChangeEvent, } from '../client/proposedApiTypes'; -import { sleep } from './core'; suite('Proposed Extension API', () => { let serviceContainer: typemoq.IMock; From 4689bea80e7c4819a36b927796ce93f76b36888a Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 20 Sep 2022 14:20:48 -0700 Subject: [PATCH 36/60] Add test --- src/client/proposedApi.ts | 4 +- src/test/proposedApi.unit.test.ts | 470 +++++++++++------------------- 2 files changed, 177 insertions(+), 297 deletions(-) diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index a4693a215c3f..8139d4edeb85 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -139,9 +139,9 @@ export function buildProposedApi( return { stateValue: RefreshStateValue.started }; } }, - async refreshEnvironments(options: RefreshOptions) { + async refreshEnvironments(options?: RefreshOptions) { await discoveryApi.triggerRefresh(undefined, { - ifNotTriggerredAlready: !options.forceRefresh, + ifNotTriggerredAlready: !options?.forceRefresh, }); }, get onDidChangeEnvironments() { diff --git a/src/test/proposedApi.unit.test.ts b/src/test/proposedApi.unit.test.ts index 201c52625c5f..272bb6e48f02 100644 --- a/src/test/proposedApi.unit.test.ts +++ b/src/test/proposedApi.unit.test.ts @@ -2,8 +2,8 @@ // Licensed under the MIT License. import * as typemoq from 'typemoq'; -import { assert } from 'chai'; -import { Uri, Event, EventEmitter } from 'vscode'; +import { assert, expect } from 'chai'; +import { Uri, Event, EventEmitter, ConfigurationTarget, WorkspaceFolder } from 'vscode'; import { IDisposableRegistry, IInterpreterPathService } from '../client/common/types'; import { IInterpreterService } from '../client/interpreter/contracts'; import { IServiceContainer } from '../client/ioc/types'; @@ -27,6 +27,8 @@ import { RefreshStateValue, ActiveEnvironmentChangeEvent, } from '../client/proposedApiTypes'; +import { PythonEnvKind, PythonEnvSource } from '../client/pythonEnvironments/base/info'; +import { Architecture } from '../client/common/utils/platform'; suite('Proposed Extension API', () => { let serviceContainer: typemoq.IMock; @@ -93,315 +95,193 @@ suite('Proposed Extension API', () => { assert.deepEqual((actual as EnvironmentReference).internal, convertCompleteEnvInfo(env)); }); - // test('getActiveInterpreterPath: With resource', async () => { - // const resource = Uri.file(__filename); - // const pythonPath = 'this/is/a/test/path'; - // interpreterService - // .setup((c) => c.getActiveInterpreter(resource)) - // .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); - // const actual = await proposed.environment.getActiveEnvironmentPath(resource); - // assert.deepEqual(actual, { path: pythonPath, pathType: 'interpreterPath' }); - // }); + test('getActiveInterpreterPath: With resource', async () => { + const pythonPath = 'this/is/a/test/path'; + const resource = Uri.file(__filename); + interpreterService + .setup((c) => c.getActiveInterpreter(resource)) + .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); + const env = buildEnvInfo({ executable: pythonPath }); + discoverAPI.setup((d) => d.resolveEnv(pythonPath)).returns(() => Promise.resolve(env)); + const actual = await proposed.environment.fetchActiveEnvironment(resource); + assert.deepEqual((actual as EnvironmentReference).internal, convertCompleteEnvInfo(env)); + }); - // test('getInterpreterDetails: no discovered python', async () => { - // discoverAPI.setup((d) => d.getEnvs()).returns(() => []); - // discoverAPI.setup((p) => p.resolveEnv(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + test('resolveEnvironment: invalid environment (when passed as string)', async () => { + const pythonPath = 'this/is/a/test/path'; + discoverAPI.setup((p) => p.resolveEnv(pythonPath)).returns(() => Promise.resolve(undefined)); - // const pythonPath = 'this/is/a/test/path (without cache)'; - // const actual = await proposed.environment.getEnvironmentDetails(pythonPath); - // expect(actual).to.be.equal(undefined); - // }); + const actual = await proposed.environment.resolveEnvironment(pythonPath); + expect(actual).to.be.equal(undefined); + }); - // test('getInterpreterDetails: no discovered python (with cache)', async () => { - // discoverAPI.setup((d) => d.getEnvs()).returns(() => []); - // discoverAPI.setup((p) => p.resolveEnv(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + test('resolveEnvironment: valid environment (when passed as string)', async () => { + const pythonPath = 'this/is/a/test/path'; + const env = buildEnvInfo({ + executable: pythonPath, + version: { + major: 3, + minor: 9, + micro: 0, + }, + kind: PythonEnvKind.System, + arch: Architecture.x64, + sysPrefix: 'prefix/path', + searchLocation: Uri.file('path/to/project'), + }); + discoverAPI.setup((p) => p.resolveEnv(pythonPath)).returns(() => Promise.resolve(env)); - // const pythonPath = 'this/is/a/test/path'; - // const actual = await proposed.environment.getEnvironmentDetails(pythonPath, { useCache: true }); - // expect(actual).to.be.equal(undefined); - // }); + const actual = await proposed.environment.resolveEnvironment(pythonPath); + assert.deepEqual((actual as EnvironmentReference).internal, convertCompleteEnvInfo(env)); + }); - // test('getInterpreterDetails: without cache', async () => { - // const pythonPath = 'this/is/a/test/path'; - - // const expected: EnvironmentDetails = { - // interpreterPath: pythonPath, - // version: ['3', '9', '0'], - // environmentType: [PythonEnvKind.System], - // metadata: { - // sysPrefix: 'prefix/path', - // bitness: Architecture.x64, - // project: Uri.file('path/to/project'), - // }, - // envFolderPath: undefined, - // }; - - // discoverAPI.setup((d) => d.getEnvs()).returns(() => []); - // discoverAPI - // .setup((p) => p.resolveEnv(pythonPath)) - // .returns(() => - // Promise.resolve( - // buildEnvInfo({ - // executable: pythonPath, - // version: { - // major: 3, - // minor: 9, - // micro: 0, - // }, - // kind: PythonEnvKind.System, - // arch: Architecture.x64, - // sysPrefix: 'prefix/path', - // searchLocation: Uri.file('path/to/project'), - // }), - // ), - // ); - - // const actual = await proposed.environment.getEnvironmentDetails(pythonPath, { useCache: false }); - // expect(actual).to.be.deep.equal(expected); - // }); + test('resolveEnvironment: valid environment (when passed as environment)', async () => { + const pythonPath = 'this/is/a/test/path'; + const env = buildEnvInfo({ + executable: pythonPath, + version: { + major: 3, + minor: 9, + micro: 0, + }, + kind: PythonEnvKind.System, + arch: Architecture.x64, + sysPrefix: 'prefix/path', + searchLocation: Uri.file('path/to/project'), + }); + const partialEnv = buildEnvInfo({ + executable: pythonPath, + kind: PythonEnvKind.System, + sysPrefix: 'prefix/path', + searchLocation: Uri.file('path/to/project'), + }); + discoverAPI.setup((p) => p.resolveEnv(pythonPath)).returns(() => Promise.resolve(env)); - // test('getInterpreterDetails: from cache', async () => { - // const pythonPath = 'this/is/a/test/path'; - - // const expected: EnvironmentDetails = { - // interpreterPath: pythonPath, - // version: ['3', '9', '0'], - // environmentType: [PythonEnvKind.System], - // metadata: { - // sysPrefix: 'prefix/path', - // bitness: Architecture.x64, - // project: undefined, - // }, - // envFolderPath: undefined, - // }; + const actual = await proposed.environment.resolveEnvironment(convertCompleteEnvInfo(partialEnv)); + assert.deepEqual((actual as EnvironmentReference).internal, convertCompleteEnvInfo(env)); + }); - // discoverAPI - // .setup((d) => d.getEnvs()) - // .returns(() => [ - // { - // executable: { - // filename: pythonPath, - // ctime: 1, - // mtime: 2, - // sysPrefix: 'prefix/path', - // }, - // version: { - // major: 3, - // minor: 9, - // micro: 0, - // }, - // kind: PythonEnvKind.System, - // arch: Architecture.x64, - // name: '', - // location: '', - // source: [PythonEnvSource.PathEnvVar], - // distro: { - // org: '', - // }, - // }, - // ]); - // discoverAPI - // .setup((p) => p.resolveEnv(pythonPath)) - // .returns(() => - // Promise.resolve( - // buildEnvInfo({ - // executable: pythonPath, - // version: { - // major: 3, - // minor: 9, - // micro: 0, - // }, - // kind: PythonEnvKind.System, - // arch: Architecture.x64, - // sysPrefix: 'prefix/path', - // }), - // ), - // ); - - // const actual = await proposed.environment.getEnvironmentDetails(pythonPath, { useCache: true }); - // expect(actual).to.be.deep.equal(expected); - // }); + test('environments: no pythons found', () => { + discoverAPI.setup((d) => d.getEnvs()).returns(() => []); + const actual = proposed.environment.environments; + expect(actual).to.be.deep.equal([]); + }); - // test('getInterpreterDetails: cache miss', async () => { - // const pythonPath = 'this/is/a/test/path'; - - // const expected: EnvironmentDetails = { - // interpreterPath: pythonPath, - // version: ['3', '9', '0'], - // environmentType: [PythonEnvKind.System], - // metadata: { - // sysPrefix: 'prefix/path', - // bitness: Architecture.x64, - // project: undefined, - // }, - // envFolderPath: undefined, - // }; - - // // Force this API to return empty to cause a cache miss. - // discoverAPI.setup((d) => d.getEnvs()).returns(() => []); - // discoverAPI - // .setup((p) => p.resolveEnv(pythonPath)) - // .returns(() => - // Promise.resolve( - // buildEnvInfo({ - // executable: pythonPath, - // version: { - // major: 3, - // minor: 9, - // micro: 0, - // }, - // kind: PythonEnvKind.System, - // arch: Architecture.x64, - // sysPrefix: 'prefix/path', - // }), - // ), - // ); - - // const actual = await proposed.environment.getEnvironmentDetails(pythonPath, { useCache: true }); - // expect(actual).to.be.deep.equal(expected); - // }); + test('environments: python found', async () => { + const envs = [ + { + executable: { + filename: 'this/is/a/test/python/path1', + ctime: 1, + mtime: 2, + sysPrefix: 'prefix/path', + }, + version: { + major: 3, + minor: 9, + micro: 0, + }, + kind: PythonEnvKind.System, + arch: Architecture.x64, + name: '', + location: '', + source: [PythonEnvSource.PathEnvVar], + distro: { + org: '', + }, + }, + { + executable: { + filename: 'this/is/a/test/python/path2', + ctime: 1, + mtime: 2, + sysPrefix: 'prefix/path', + }, + version: { + major: 3, + minor: 10, + micro: 0, + }, + kind: PythonEnvKind.Venv, + arch: Architecture.x64, + name: '', + location: '', + source: [PythonEnvSource.PathEnvVar], + distro: { + org: '', + }, + }, + ]; + discoverAPI.setup((d) => d.getEnvs()).returns(() => envs); + const actual = proposed.environment.environments; + const actualEnvs = actual?.map((a) => (a as EnvironmentReference).internal); + assert.deepEqual( + actualEnvs?.sort((a, b) => a.pathID.localeCompare(b.pathID)), + envs.map((e) => convertCompleteEnvInfo(e)).sort((a, b) => a.pathID.localeCompare(b.pathID)), + ); + }); - // test('getInterpreterPaths: no pythons found', async () => { - // discoverAPI.setup((d) => d.getEnvs()).returns(() => []); - // const actual = await proposed.environment.getEnvironmentPaths(); - // expect(actual).to.be.deep.equal([]); - // }); + test('updateActiveEnvironment: no resource', async () => { + interpreterPathService + .setup((i) => i.update(undefined, ConfigurationTarget.WorkspaceFolder, 'this/is/a/test/python/path')) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); - // test('getInterpreterPaths: python found', async () => { - // discoverAPI - // .setup((d) => d.getEnvs()) - // .returns(() => [ - // { - // executable: { - // filename: 'this/is/a/test/python/path1', - // ctime: 1, - // mtime: 2, - // sysPrefix: 'prefix/path', - // }, - // version: { - // major: 3, - // minor: 9, - // micro: 0, - // }, - // kind: PythonEnvKind.System, - // arch: Architecture.x64, - // name: '', - // location: '', - // source: [PythonEnvSource.PathEnvVar], - // distro: { - // org: '', - // }, - // }, - // { - // executable: { - // filename: 'this/is/a/test/python/path2', - // ctime: 1, - // mtime: 2, - // sysPrefix: 'prefix/path', - // }, - // version: { - // major: 3, - // minor: 10, - // micro: 0, - // }, - // kind: PythonEnvKind.Venv, - // arch: Architecture.x64, - // name: '', - // location: '', - // source: [PythonEnvSource.PathEnvVar], - // distro: { - // org: '', - // }, - // }, - // ]); - // const actual = await proposed.environment.getEnvironmentPaths(); - // expect(actual?.map((a) => a.path)).to.be.deep.equal([ - // 'this/is/a/test/python/path1', - // 'this/is/a/test/python/path2', - // ]); - // }); + await proposed.environment.updateActiveEnvironment('this/is/a/test/python/path'); - // test('setActiveInterpreter: no resource', async () => { - // interpreterPathService - // .setup((i) => i.update(undefined, ConfigurationTarget.WorkspaceFolder, 'this/is/a/test/python/path')) - // .returns(() => Promise.resolve()) - // .verifiable(typemoq.Times.once()); + interpreterPathService.verifyAll(); + }); - // await proposed.environment.setActiveEnvironment('this/is/a/test/python/path'); + test('setActiveInterpreter: with uri', async () => { + const uri = Uri.parse('a'); + interpreterPathService + .setup((i) => i.update(uri, ConfigurationTarget.WorkspaceFolder, 'this/is/a/test/python/path')) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); - // interpreterPathService.verifyAll(); - // }); - // test('setActiveInterpreter: with resource', async () => { - // const resource = Uri.parse('a'); - // interpreterPathService - // .setup((i) => i.update(resource, ConfigurationTarget.WorkspaceFolder, 'this/is/a/test/python/path')) - // .returns(() => Promise.resolve()) - // .verifiable(typemoq.Times.once()); + await proposed.environment.updateActiveEnvironment('this/is/a/test/python/path', uri); + + interpreterPathService.verifyAll(); + }); - // await proposed.environment.setActiveEnvironment('this/is/a/test/python/path', resource); + test('setActiveInterpreter: with workspace folder', async () => { + const uri = Uri.parse('a'); + interpreterPathService + .setup((i) => i.update(uri, ConfigurationTarget.WorkspaceFolder, 'this/is/a/test/python/path')) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + const workspace: WorkspaceFolder = { + uri, + name: '', + index: 0, + }; + + await proposed.environment.updateActiveEnvironment('this/is/a/test/python/path', workspace); + + interpreterPathService.verifyAll(); + }); - // interpreterPathService.verifyAll(); - // }); + test('refreshInterpreters: default', async () => { + discoverAPI + .setup((d) => d.triggerRefresh(undefined, typemoq.It.isValue({ ifNotTriggerredAlready: true }))) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); - // test('refreshInterpreters: common scenario', async () => { - // discoverAPI - // .setup((d) => d.triggerRefresh(undefined, undefined)) - // .returns(() => Promise.resolve()) - // .verifiable(typemoq.Times.once()); - // discoverAPI - // .setup((d) => d.getEnvs()) - // .returns(() => [ - // { - // executable: { - // filename: 'this/is/a/test/python/path1', - // ctime: 1, - // mtime: 2, - // sysPrefix: 'prefix/path', - // }, - // version: { - // major: 3, - // minor: 9, - // micro: 0, - // }, - // kind: PythonEnvKind.System, - // arch: Architecture.x64, - // name: '', - // location: 'this/is/a/test/python/path1/folder', - // source: [PythonEnvSource.PathEnvVar], - // distro: { - // org: '', - // }, - // }, - // { - // executable: { - // filename: 'this/is/a/test/python/path2', - // ctime: 1, - // mtime: 2, - // sysPrefix: 'prefix/path', - // }, - // version: { - // major: 3, - // minor: 10, - // micro: 0, - // }, - // kind: PythonEnvKind.Venv, - // arch: Architecture.x64, - // name: '', - // location: '', - // source: [PythonEnvSource.PathEnvVar], - // distro: { - // org: '', - // }, - // }, - // ]); - - // const actual = await proposed.environment.refreshEnvironment(); - // expect(actual).to.be.deep.equal([ - // { path: 'this/is/a/test/python/path1/folder', pathType: 'envFolderPath' }, - // { path: 'this/is/a/test/python/path2', pathType: 'interpreterPath' }, - // ]); - // discoverAPI.verifyAll(); - // }); + await proposed.environment.refreshEnvironments(); + + discoverAPI.verifyAll(); + }); + + test('refreshInterpreters: when forcing a refresh', async () => { + discoverAPI + .setup((d) => d.triggerRefresh(undefined, typemoq.It.isValue({ ifNotTriggerredAlready: false }))) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + await proposed.environment.refreshEnvironments({ forceRefresh: true }); + + discoverAPI.verifyAll(); + }); // test('getRefreshPromise: common scenario', () => { // const expected = Promise.resolve(); From 4e436cd0a99fefa902fc85a97ac21a0adeff55b8 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 20 Sep 2022 15:00:02 -0700 Subject: [PATCH 37/60] Fix some tests --- src/client/proposedApi.ts | 8 ++- src/test/proposedApi.unit.test.ts | 107 ++++++++++++++++++++++++++++-- 2 files changed, 108 insertions(+), 7 deletions(-) diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index 8139d4edeb85..299caef96ec6 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -226,7 +226,7 @@ function convertKind(kind: PythonEnvKind): EnvironmentTools | undefined { } } -function convertEnvInfoAndGetReference(env: PythonEnvInfo): Environment { +export function convertEnvInfo(env: PythonEnvInfo): Environment { const convertedEnv = convertCompleteEnvInfo(env) as Environment; if (convertedEnv.executable.sysPrefix === '') { convertedEnv.executable.sysPrefix = undefined; @@ -246,7 +246,11 @@ function convertEnvInfoAndGetReference(env: PythonEnvInfo): Environment { if (convertedEnv.version.minor === -1) { convertedEnv.version.minor = undefined; } - return getEnvReference(convertedEnv); + return convertedEnv; +} + +function convertEnvInfoAndGetReference(env: PythonEnvInfo): Environment { + return getEnvReference(convertEnvInfo(env)); } function convertArch(arch: Architecture) { diff --git a/src/test/proposedApi.unit.test.ts b/src/test/proposedApi.unit.test.ts index 272bb6e48f02..587f4ff00c88 100644 --- a/src/test/proposedApi.unit.test.ts +++ b/src/test/proposedApi.unit.test.ts @@ -4,12 +4,14 @@ import * as typemoq from 'typemoq'; import { assert, expect } from 'chai'; import { Uri, Event, EventEmitter, ConfigurationTarget, WorkspaceFolder } from 'vscode'; +import { cloneDeep } from 'lodash'; import { IDisposableRegistry, IInterpreterPathService } from '../client/common/types'; import { IInterpreterService } from '../client/interpreter/contracts'; import { IServiceContainer } from '../client/ioc/types'; import { buildProposedApi, convertCompleteEnvInfo, + convertEnvInfo, EnvironmentReference, reportActiveInterpreterChanged, } from '../client/proposedApi'; @@ -26,9 +28,11 @@ import { RefreshState, RefreshStateValue, ActiveEnvironmentChangeEvent, + EnvironmentsChangedEvent, } from '../client/proposedApiTypes'; import { PythonEnvKind, PythonEnvSource } from '../client/pythonEnvironments/base/info'; import { Architecture } from '../client/common/utils/platform'; +import { PythonEnvCollectionChangedEvent } from '../client/pythonEnvironments/base/watcher'; suite('Proposed Extension API', () => { let serviceContainer: typemoq.IMock; @@ -37,6 +41,7 @@ suite('Proposed Extension API', () => { let interpreterService: typemoq.IMock; let onDidExecutionEvent: Event; let onDidChangeRefreshState: EventEmitter; + let onDidChangeEnvironments: EventEmitter; let proposed: ProposedExtensionAPI; @@ -47,6 +52,7 @@ suite('Proposed Extension API', () => { interpreterService = typemoq.Mock.ofType(); onDidExecutionEvent = typemoq.Mock.ofType>().object; onDidChangeRefreshState = new EventEmitter(); + onDidChangeEnvironments = new EventEmitter(); interpreterService.setup((i) => i.onDidChangeInterpreterConfiguration).returns(() => onDidExecutionEvent); serviceContainer.setup((s) => s.get(IInterpreterPathService)).returns(() => interpreterPathService.object); @@ -54,11 +60,12 @@ suite('Proposed Extension API', () => { serviceContainer.setup((s) => s.get(IDisposableRegistry)).returns(() => []); discoverAPI.setup((d) => d.onProgress).returns(() => onDidChangeRefreshState.event); + discoverAPI.setup((d) => d.onChanged).returns(() => onDidChangeEnvironments.event); proposed = buildProposedApi(discoverAPI.object, serviceContainer.object); }); - test('Provides a event for tracking refresh progress', async () => { + test('Provides an event for tracking refresh progress', async () => { const events: RefreshState[] = []; proposed.environment.onDidChangeRefreshState((e) => { events.push(e); @@ -199,8 +206,8 @@ suite('Proposed Extension API', () => { }, version: { major: 3, - minor: 10, - micro: 0, + minor: -1, + micro: -1, }, kind: PythonEnvKind.Venv, arch: Architecture.x64, @@ -217,10 +224,100 @@ suite('Proposed Extension API', () => { const actualEnvs = actual?.map((a) => (a as EnvironmentReference).internal); assert.deepEqual( actualEnvs?.sort((a, b) => a.pathID.localeCompare(b.pathID)), - envs.map((e) => convertCompleteEnvInfo(e)).sort((a, b) => a.pathID.localeCompare(b.pathID)), + envs.map((e) => convertEnvInfo(e)).sort((a, b) => a.pathID.localeCompare(b.pathID)), ); }); + test('Provide an event to track when list of environments change', async () => { + let events: EnvironmentsChangedEvent[] = []; + let eventValues: EnvironmentsChangedEvent[] = []; + let expectedEvents: EnvironmentsChangedEvent[] = []; + proposed.environment.onDidChangeEnvironments((e) => { + events.push(e); + }); + const envs = [ + buildEnvInfo({ + executable: 'pythonPath', + kind: PythonEnvKind.System, + sysPrefix: 'prefix/path', + searchLocation: Uri.file('path/to/project'), + }), + { + executable: { + filename: 'this/is/a/test/python/path1', + ctime: 1, + mtime: 2, + sysPrefix: 'prefix/path', + }, + version: { + major: 3, + minor: 9, + micro: 0, + }, + kind: PythonEnvKind.System, + arch: Architecture.x64, + name: '', + location: '', + source: [PythonEnvSource.PathEnvVar], + distro: { + org: '', + }, + }, + { + executable: { + filename: 'this/is/a/test/python/path2', + ctime: 1, + mtime: 2, + sysPrefix: 'prefix/path', + }, + version: { + major: 3, + minor: 10, + micro: 0, + }, + kind: PythonEnvKind.Venv, + arch: Architecture.x64, + name: '', + location: '', + source: [PythonEnvSource.PathEnvVar], + distro: { + org: '', + }, + }, + ]; + + // Now fire and verify events. Note the event value holds the reference to an environment, so may itself + // change when the environment is altered. So it's important to verify them as soon as they're received. + + // Add events + onDidChangeEnvironments.fire({ old: undefined, new: envs[0] }); + expectedEvents.push({ env: convertEnvInfo(envs[0]), type: 'add' }); + onDidChangeEnvironments.fire({ old: undefined, new: envs[1] }); + expectedEvents.push({ env: convertEnvInfo(envs[1]), type: 'add' }); + onDidChangeEnvironments.fire({ old: undefined, new: envs[2] }); + expectedEvents.push({ env: convertEnvInfo(envs[2]), type: 'add' }); + eventValues = events.map((e) => ({ env: (e.env as EnvironmentReference).internal, type: e.type })); + assert.deepEqual(eventValues, expectedEvents); + + // Update events + events = []; + expectedEvents = []; + const updatedEnv = cloneDeep(envs[0]); + updatedEnv.arch = Architecture.x86; + onDidChangeEnvironments.fire({ old: envs[0], new: updatedEnv }); + expectedEvents.push({ env: convertEnvInfo(updatedEnv), type: 'update' }); + eventValues = events.map((e) => ({ env: (e.env as EnvironmentReference).internal, type: e.type })); + assert.deepEqual(eventValues, expectedEvents); + + // Remove events + events = []; + expectedEvents = []; + onDidChangeEnvironments.fire({ old: envs[2], new: undefined }); + expectedEvents.push({ env: convertEnvInfo(envs[2]), type: 'remove' }); + eventValues = events.map((e) => ({ env: (e.env as EnvironmentReference).internal, type: e.type })); + assert.deepEqual(eventValues, expectedEvents); + }); + test('updateActiveEnvironment: no resource', async () => { interpreterPathService .setup((i) => i.update(undefined, ConfigurationTarget.WorkspaceFolder, 'this/is/a/test/python/path')) @@ -283,7 +380,7 @@ suite('Proposed Extension API', () => { discoverAPI.verifyAll(); }); - // test('getRefreshPromise: common scenario', () => { + // test('Verify refreshState are converted and passed along appropriately', () => { // const expected = Promise.resolve(); // discoverAPI // .setup((d) => d.getRefreshPromise(typemoq.It.isValue({ stage: ProgressReportStage.allPathsDiscovered }))) From bd6f16dc53befe2c5ed950c5995008ddc4431dd3 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 20 Sep 2022 15:32:39 -0700 Subject: [PATCH 38/60] Add test for refresh state --- src/test/proposedApi.unit.test.ts | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/test/proposedApi.unit.test.ts b/src/test/proposedApi.unit.test.ts index 587f4ff00c88..c038358c3d54 100644 --- a/src/test/proposedApi.unit.test.ts +++ b/src/test/proposedApi.unit.test.ts @@ -380,16 +380,18 @@ suite('Proposed Extension API', () => { discoverAPI.verifyAll(); }); - // test('Verify refreshState are converted and passed along appropriately', () => { - // const expected = Promise.resolve(); - // discoverAPI - // .setup((d) => d.getRefreshPromise(typemoq.It.isValue({ stage: ProgressReportStage.allPathsDiscovered }))) - // .returns(() => expected); - // const actual = proposed.environment.getRefreshPromise({ stage: ProgressReportStage.allPathsDiscovered }); - - // // We are comparing instances here, they should be the same instance. - // // So '==' is ok here. - // // eslint-disable-next-line eqeqeq - // expect(actual == expected).is.equal(true); - // }); + test('Verify refreshStates are passed along appropriately', () => { + discoverAPI.setup((d) => d.refreshState).returns(() => ProgressReportStage.discoveryStarted); + assert.deepEqual(proposed.environment.refreshState, { + stateValue: RefreshStateValue.started, + }); + discoverAPI.setup((d) => d.refreshState).returns(() => ProgressReportStage.allPathsDiscovered); + assert.deepEqual(proposed.environment.refreshState, { + stateValue: RefreshStateValue.started, + }); + discoverAPI.setup((d) => d.refreshState).returns(() => ProgressReportStage.discoveryFinished); + assert.deepEqual(proposed.environment.refreshState, { + stateValue: RefreshStateValue.finished, + }); + }); }); From 5edeab1035feff107a9111aa9befcfdc6487a388 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 20 Sep 2022 15:46:14 -0700 Subject: [PATCH 39/60] Rearrange types --- src/client/proposedApiTypes.ts | 90 +++++++++++++++++----------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index 2f7b547e80a6..0e1aa4815ecf 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -28,22 +28,22 @@ interface EnvironmentAPI { /** * This event is triggered when the active environment changes. */ - onDidChangeActiveEnvironment: Event; + readonly onDidChangeActiveEnvironment: Event; /** * Carries environments found by the extension at the time of fetching the property. Note a refresh might be * going on so this may not be the complete list. To wait on complete list use {@link refreshState()} and * {@link onDidChangeRefreshState}. */ - environments: readonly Environment[] | undefined; + readonly environments: Environment[] | 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; + readonly onDidChangeEnvironments: Event; /** * Carries the current state in the refresh, i.e whether it started, finished, or any other relevant state. */ - refreshState: RefreshState; + readonly refreshState: RefreshState; /** * Fires when a refresh state has been reached, i.e when it starts, finishes or any other relevant state. * Tracks refresh progress for current list of known environments. @@ -68,11 +68,40 @@ interface EnvironmentAPI { */ resolveEnvironment(environment: Environment | UniquePath): Promise; /** - * @deprecated Use {@link fetchActiveEnvironment} instead. + * @deprecated Use {@link fetchActiveEnvironment} instead. This will soon be removed. */ getActiveEnvironmentPath(resource?: Resource): Promise; } +export type RefreshState = { + stateValue: RefreshStateValue; +}; + +/** + * Contains state values in the order they finish during a refresh cycle. + */ +export enum RefreshStateValue { + /** + * When a refresh is started. + */ + started = 0, + + // ...there can be more intimidatory states + + /** + * When a refresh is over. + */ + finished = 1, +} + +export type RefreshOptions = { + /** + * Force trigger a refresh regardless of whether a refresh was already triggered. Note this can be expensive so + * it's best to only use it if user manually triggers a refresh. + */ + forceRefresh?: boolean; +}; + /** * Details about the environment. Note the environment folder, type and name never changes over time. */ @@ -182,39 +211,6 @@ export interface ResolvedEnvironment { tools: EnvironmentTools[] | undefined; } -export type RefreshState = { - stateValue: RefreshStateValue; -}; - -/** - * Contains state values in the order they finish during a refresh cycle. - */ -export enum RefreshStateValue { - /** - * When a refresh is started. - */ - started = 0, - - // ...there can be more intimidatory states - - /** - * When a refresh is over. - */ - finished = 1, -} - -/** - * 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 UniquePath = string; - export type EnvironmentsChangedEvent = { env: Environment; /** @@ -236,13 +232,17 @@ export type ActiveEnvironmentChangeEvent = { resource: WorkspaceFolder | undefined; }; -export type RefreshOptions = { - /** - * Force trigger a refresh regardless of whether a refresh was already triggered. Note this can be expensive so - * it's best to only use it if user manually triggers a refresh. - */ - forceRefresh?: boolean; -}; +/** + * 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 UniquePath = string; /** * Tool/plugin where the environment came from. It can be {@link KnownEnvironmentTools} or custom string which From c657d9142cbfe51decb0c29e7f029c18d21bdf9e Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 20 Sep 2022 15:52:20 -0700 Subject: [PATCH 40/60] Fix tests --- .../interpreters/interpreterService.unit.test.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/test/interpreters/interpreterService.unit.test.ts b/src/test/interpreters/interpreterService.unit.test.ts index 4ab2c8086309..c7f770ffa5b3 100644 --- a/src/test/interpreters/interpreterService.unit.test.ts +++ b/src/test/interpreters/interpreterService.unit.test.ts @@ -248,6 +248,8 @@ suite('Interpreters service', () => { test('If stored setting is an empty string, refresh the interpreter display', async () => { const service = new InterpreterService(serviceContainer, pyenvs.object); const resource = Uri.parse('a'); + const workspaceFolder = { uri: resource, name: '', index: 0 }; + workspace.setup((w) => w.getWorkspaceFolder(resource)).returns(() => workspaceFolder); service._pythonPathSetting = ''; configService.reset(); configService.setup((c) => c.getSettings(resource)).returns(() => ({ pythonPath: 'current path' } as any)); @@ -258,14 +260,16 @@ suite('Interpreters service', () => { await service._onConfigChanged(resource); interpreterDisplay.verifyAll(); sinon.assert.calledOnceWithExactly(reportActiveInterpreterChangedStub, { - path: 'current path', - resource, + pathID: 'current path', + resource: workspaceFolder, }); }); test('If stored setting is not equal to current interpreter path setting, refresh the interpreter display', async () => { const service = new InterpreterService(serviceContainer, pyenvs.object); const resource = Uri.parse('a'); + const workspaceFolder = { uri: resource, name: '', index: 0 }; + workspace.setup((w) => w.getWorkspaceFolder(resource)).returns(() => workspaceFolder); service._pythonPathSetting = 'stored setting'; configService.reset(); configService.setup((c) => c.getSettings(resource)).returns(() => ({ pythonPath: 'current path' } as any)); @@ -276,8 +280,8 @@ suite('Interpreters service', () => { await service._onConfigChanged(resource); interpreterDisplay.verifyAll(); sinon.assert.calledOnceWithExactly(reportActiveInterpreterChangedStub, { - path: 'current path', - resource, + pathID: 'current path', + resource: workspaceFolder, }); }); From 0f7b55dea10b9af28b8c83267ce8e38386e714e5 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Tue, 20 Sep 2022 18:36:00 -0700 Subject: [PATCH 41/60] Modify activeEnvironment API --- src/client/interpreter/interpreterService.ts | 3 +- src/client/proposedApi.ts | 67 +++++++++++++------ src/client/proposedApiTypes.ts | 55 +++++++-------- .../interpreterService.unit.test.ts | 6 +- src/test/proposedApi.unit.test.ts | 18 ++--- 5 files changed, 88 insertions(+), 61 deletions(-) diff --git a/src/client/interpreter/interpreterService.ts b/src/client/interpreter/interpreterService.ts index 270280e500e2..57a5ef2521e0 100644 --- a/src/client/interpreter/interpreterService.ts +++ b/src/client/interpreter/interpreterService.ts @@ -221,7 +221,8 @@ export class InterpreterService implements Disposable, IInterpreterService { this._pythonPathSetting = pySettings.pythonPath; this.didChangeInterpreterEmitter.fire(); reportActiveInterpreterChanged({ - pathID: pySettings.pythonPath, + id: pySettings.pythonPath, + path: pySettings.pythonPath, resource: this.serviceContainer.get(IWorkspaceService).getWorkspaceFolder(resource), }); const interpreterDisplay = this.serviceContainer.get(IInterpreterDisplay); diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index 299caef96ec6..129b5101dcda 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -3,17 +3,17 @@ // Licensed under the MIT License. import { ConfigurationTarget, EventEmitter, Uri } from 'vscode'; -import { IDisposableRegistry, IInterpreterPathService } from './common/types'; +import * as pathUtils from 'path'; +import { IConfigurationService, IDisposableRegistry, IInterpreterPathService } from './common/types'; import { Architecture } from './common/utils/platform'; import { IInterpreterService } from './interpreter/contracts'; import { IServiceContainer } from './ioc/types'; import { - ActiveEnvironmentChangeEvent, + ActiveEnvironmentSettingChangeEvent, Environment, EnvironmentsChangedEvent, ProposedExtensionAPI, ResolvedEnvironment, - UniquePath, PythonVersionInfo, RefreshOptions, Resource, @@ -25,20 +25,22 @@ import { import { PythonEnvInfo, PythonEnvKind, PythonEnvType } from './pythonEnvironments/base/info'; import { getEnvPath } from './pythonEnvironments/base/info/env'; import { IDiscoveryAPI, ProgressReportStage } from './pythonEnvironments/base/locator'; +import { IPythonExecutionFactory } from './common/process/types'; +import { traceError } from './logging'; -const onDidActiveInterpreterChangedEvent = new EventEmitter(); -export function reportActiveInterpreterChanged(e: ActiveEnvironmentChangeEvent): void { +const onDidActiveInterpreterChangedEvent = new EventEmitter(); +export function reportActiveInterpreterChanged(e: ActiveEnvironmentSettingChangeEvent): void { onDidActiveInterpreterChangedEvent.fire(e); } const onProgress = new EventEmitter(); const onEnvironmentsChanged = new EventEmitter(); -const environmentsReference = new Map(); +const environmentsReference = new Map(); export class EnvironmentReference implements Environment { - readonly pathID: string; + readonly id: string; constructor(public internal: Environment) { - this.pathID = internal.pathID; + this.id = internal.id; } get executable() { @@ -63,13 +65,13 @@ export class EnvironmentReference implements Environment { } function getEnvReference(e: Environment) { - let envClass = environmentsReference.get(e.pathID); + let envClass = environmentsReference.get(e.id); if (!envClass) { envClass = new EnvironmentReference(e); } else { envClass.updateEnv(e); } - environmentsReference.set(e.pathID, envClass); + environmentsReference.set(e.id, envClass); return envClass; } @@ -79,6 +81,7 @@ export function buildProposedApi( ): ProposedExtensionAPI { const interpreterPathService = serviceContainer.get(IInterpreterPathService); const interpreterService = serviceContainer.get(IInterpreterService); + const configService = serviceContainer.get(IConfigurationService); const disposables = serviceContainer.get(IDisposableRegistry); disposables.push( discoveryApi.onProgress((e) => { @@ -105,22 +108,42 @@ export function buildProposedApi( ); const proposed: ProposedExtensionAPI = { environment: { - async fetchActiveEnvironment(resource?: Resource) { + getActiveEnvironmentSetting(resource?: Resource) { resource = resource && 'uri' in resource ? resource.uri : resource; - const env = await interpreterService.getActiveInterpreter(resource); - if (!env) { - return undefined; - } - return resolveEnvironment(getEnvPath(env.path, env.envPath).path, discoveryApi); + const path = configService.getSettings(resource).pythonPath; + const id = path === 'python' ? 'defaultPython' : path; + return { id, path }; }, - updateActiveEnvironment(env: string | Environment, resource?: Resource): Promise { - const path = typeof env !== 'string' ? env.pathID : env; + updateActiveEnvironmentSetting(env: string | Environment, resource?: Resource): Promise { + const path = typeof env !== 'string' ? env.id : env; resource = resource && 'uri' in resource ? resource.uri : resource; return interpreterPathService.update(resource, ConfigurationTarget.WorkspaceFolder, path); }, - onDidChangeActiveEnvironment: onDidActiveInterpreterChangedEvent.event, - resolveEnvironment: (env: string | Environment) => { - const path = typeof env !== 'string' ? env.pathID : env; + onDidChangeActiveEnvironmentSetting: onDidActiveInterpreterChangedEvent.event, + resolveEnvironment: async (env: string | Environment) => { + let path = typeof env !== 'string' ? env.id : env; + if (pathUtils.basename(path) === path) { + // Value can be `python`, `python3`, `python3.9` etc. + // Note the following triggers autoselection if no interpreter is explictly + // selected, i.e the value is `python`. + // During shutdown we might not be able to get items out of the service container. + const pythonExecutionFactory = serviceContainer.tryGet( + IPythonExecutionFactory, + ); + const pythonExecutionService = pythonExecutionFactory + ? await pythonExecutionFactory.create({ pythonPath: path }) + : undefined; + const fullyQualifiedPath = pythonExecutionService + ? await pythonExecutionService.getExecutablePath().catch((ex) => { + traceError(ex); + }) + : undefined; + // Python path is invalid or python isn't installed. + if (!fullyQualifiedPath) { + return undefined; + } + path = fullyQualifiedPath; + } return resolveEnvironment(path, discoveryApi); }, get environments(): Environment[] { @@ -175,7 +198,7 @@ export function convertCompleteEnvInfo(env: PythonEnvInfo): ResolvedEnvironment tool = 'Unknown'; } const resolvedEnv: ResolvedEnvironment = { - pathID: getEnvPath(env.executable.filename, env.location).path, + id: getEnvPath(env.executable.filename, env.location).path, executable: { uri: Uri.file(env.executable.filename), bitness: convertArch(env.arch), diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index 0e1aa4815ecf..36bbdddf66e4 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -11,12 +11,11 @@ export interface ProposedExtensionAPI { interface EnvironmentAPI { /** - * Returns the environment selected. Uses the cache by default, otherwise fetches full information about the - * environment. + * Returns the environment configured by user in settings. * @param resource : Uri of a file or workspace folder. This is used to determine the env in a multi-root * scenario. If `undefined`, then the API returns what ever is set for the workspace. */ - fetchActiveEnvironment(resource?: Resource): Promise; + getActiveEnvironmentSetting(resource?: Resource): ActiveEnvironmentSetting; /** * Sets the active environment path for the python extension for the resource. Configuration target will always * be the workspace folder. @@ -24,11 +23,11 @@ interface EnvironmentAPI { * the environment itself. * @param resource : [optional] File or workspace to scope to a particular workspace folder. */ - updateActiveEnvironment(environment: Environment | UniquePath, resource?: Resource): Promise; + updateActiveEnvironmentSetting(environment: Environment | string, resource?: Resource): Promise; /** - * This event is triggered when the active environment changes. + * This event is triggered when the active environment setting changes. */ - readonly onDidChangeActiveEnvironment: Event; + readonly onDidChangeActiveEnvironmentSetting: Event; /** * Carries environments found by the extension at the time of fetching the property. Note a refresh might be * going on so this may not be the complete list. To wait on complete list use {@link refreshState()} and @@ -63,12 +62,12 @@ interface EnvironmentAPI { refreshEnvironments(options?: RefreshOptions, token?: CancellationToken): Promise; /** * Returns details for the given environment, or `undefined` if the env is invalid. - * @param environment : Environment whose details you need. Can also pass the full path to environment folder - * or python executable for the environment. + * @param environment : Full path to environment folder or python executable for the environment. Can also pass + * the environment id or the environment itself. */ - resolveEnvironment(environment: Environment | UniquePath): Promise; + resolveEnvironment(environment: Environment | string): Promise; /** - * @deprecated Use {@link fetchActiveEnvironment} instead. This will soon be removed. + * @deprecated Use {@link getActiveEnvironmentSetting} instead. This will soon be removed. */ getActiveEnvironmentPath(resource?: Resource): Promise; } @@ -107,9 +106,9 @@ export type RefreshOptions = { */ export type Environment = { /** - * See {@link UniquePath} for description. + * The unique ID of the environment. */ - pathID: UniquePath; + id: string; /** * Carries details about python executable. */ @@ -162,7 +161,7 @@ export type Environment = { }; /** * Tools/plugins which created the environment or where it came from. First value in array corresponds - * to the primary tool responsible for the environment, which never changes over time. + * to the primary tool which manages the environment, which never changes over time. */ tools: EnvironmentTools[] | undefined; }; @@ -174,7 +173,7 @@ type MakeAllPropertiesNonNullable = { [P in keyof T]: NonNullable; }; /** - * A new form of object `Type` where a property represented by `Key` cannot be `undefined`. + * A new form of object `Type` where a specific property `Key` cannot be `undefined`. */ type MakePropertyNonNullable = Omit & MakeAllPropertiesNonNullable>; @@ -189,9 +188,9 @@ export type PythonVersionInfo = MakeAllPropertiesNonNullable { await service._onConfigChanged(resource); interpreterDisplay.verifyAll(); sinon.assert.calledOnceWithExactly(reportActiveInterpreterChangedStub, { - pathID: 'current path', + id: 'current path', + path: 'current path', resource: workspaceFolder, }); }); @@ -280,7 +281,8 @@ suite('Interpreters service', () => { await service._onConfigChanged(resource); interpreterDisplay.verifyAll(); sinon.assert.calledOnceWithExactly(reportActiveInterpreterChangedStub, { - pathID: 'current path', + id: 'current path', + path: 'current path', resource: workspaceFolder, }); }); diff --git a/src/test/proposedApi.unit.test.ts b/src/test/proposedApi.unit.test.ts index c038358c3d54..5c45bf24f7f0 100644 --- a/src/test/proposedApi.unit.test.ts +++ b/src/test/proposedApi.unit.test.ts @@ -27,7 +27,7 @@ import { ProposedExtensionAPI, RefreshState, RefreshStateValue, - ActiveEnvironmentChangeEvent, + ActiveEnvironmentSettingChangeEvent, EnvironmentsChangedEvent, } from '../client/proposedApiTypes'; import { PythonEnvKind, PythonEnvSource } from '../client/pythonEnvironments/base/info'; @@ -82,16 +82,16 @@ suite('Proposed Extension API', () => { }); test('Provide an event to track when active environment details change', async () => { - const events: ActiveEnvironmentChangeEvent[] = []; - proposed.environment.onDidChangeActiveEnvironment((e) => { + const events: ActiveEnvironmentSettingChangeEvent[] = []; + proposed.environment.onDidChangeActiveEnvironmentSetting((e) => { events.push(e); }); - reportActiveInterpreterChanged({ pathID: 'path/to/environment', resource: undefined }); + reportActiveInterpreterChanged({ id: 'path/to/environment', path: 'path/to/environment', resource: undefined }); await sleep(1); - assert.deepEqual(events, [{ pathID: 'path/to/environment', resource: undefined }]); + assert.deepEqual(events, [{ id: 'path/to/environment', path: 'path/to/environment', resource: undefined }]); }); - test('getActiveInterpreterPath: No resource', async () => { + test('getActiveEnvironmentSetting: No resource', async () => { const pythonPath = 'this/is/a/test/path'; interpreterService .setup((c) => c.getActiveInterpreter(undefined)) @@ -102,7 +102,7 @@ suite('Proposed Extension API', () => { assert.deepEqual((actual as EnvironmentReference).internal, convertCompleteEnvInfo(env)); }); - test('getActiveInterpreterPath: With resource', async () => { + test('getActiveEnvironmentSetting: With resource', async () => { const pythonPath = 'this/is/a/test/path'; const resource = Uri.file(__filename); interpreterService @@ -223,8 +223,8 @@ suite('Proposed Extension API', () => { const actual = proposed.environment.environments; const actualEnvs = actual?.map((a) => (a as EnvironmentReference).internal); assert.deepEqual( - actualEnvs?.sort((a, b) => a.pathID.localeCompare(b.pathID)), - envs.map((e) => convertEnvInfo(e)).sort((a, b) => a.pathID.localeCompare(b.pathID)), + actualEnvs?.sort((a, b) => a.id.localeCompare(b.id)), + envs.map((e) => convertEnvInfo(e)).sort((a, b) => a.id.localeCompare(b.id)), ); }); From bd7cd674b899c9e2067906f2e594f8493bd59a4c Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 21 Sep 2022 14:08:00 -0700 Subject: [PATCH 42/60] API sync --- src/client/interpreter/interpreterService.ts | 1 - src/client/proposedApi.ts | 94 +++++------- src/client/proposedApiTypes.ts | 131 ++++++----------- .../interpreterService.unit.test.ts | 2 - src/test/proposedApi.unit.test.ts | 138 ++++++++---------- 5 files changed, 149 insertions(+), 217 deletions(-) diff --git a/src/client/interpreter/interpreterService.ts b/src/client/interpreter/interpreterService.ts index 57a5ef2521e0..50545558d721 100644 --- a/src/client/interpreter/interpreterService.ts +++ b/src/client/interpreter/interpreterService.ts @@ -221,7 +221,6 @@ export class InterpreterService implements Disposable, IInterpreterService { this._pythonPathSetting = pySettings.pythonPath; this.didChangeInterpreterEmitter.fire(); reportActiveInterpreterChanged({ - id: pySettings.pythonPath, path: pySettings.pythonPath, resource: this.serviceContainer.get(IWorkspaceService).getWorkspaceFolder(resource), }); diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index 129b5101dcda..f117e1d0826a 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -2,37 +2,40 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { ConfigurationTarget, EventEmitter, Uri } from 'vscode'; +import { ConfigurationTarget, EventEmitter, Uri, WorkspaceFolder } from 'vscode'; import * as pathUtils from 'path'; import { IConfigurationService, IDisposableRegistry, IInterpreterPathService } from './common/types'; import { Architecture } from './common/utils/platform'; import { IInterpreterService } from './interpreter/contracts'; import { IServiceContainer } from './ioc/types'; import { - ActiveEnvironmentSettingChangeEvent, + ActiveEnvironmentIdChangeEvent, Environment, EnvironmentsChangedEvent, ProposedExtensionAPI, ResolvedEnvironment, - PythonVersionInfo, RefreshOptions, Resource, - RefreshStateValue, - RefreshState, EnvironmentType, EnvironmentTools, + EnvironmentId, } from './proposedApiTypes'; import { PythonEnvInfo, PythonEnvKind, PythonEnvType } from './pythonEnvironments/base/info'; import { getEnvPath } from './pythonEnvironments/base/info/env'; -import { IDiscoveryAPI, ProgressReportStage } from './pythonEnvironments/base/locator'; +import { IDiscoveryAPI } from './pythonEnvironments/base/locator'; import { IPythonExecutionFactory } from './common/process/types'; import { traceError } from './logging'; +import { normCasePath } from './common/platform/fs-paths'; -const onDidActiveInterpreterChangedEvent = new EventEmitter(); -export function reportActiveInterpreterChanged(e: ActiveEnvironmentSettingChangeEvent): void { - onDidActiveInterpreterChangedEvent.fire(e); +type ActiveEnvironmentChangeEvent = { + resource: WorkspaceFolder | undefined; + path: string; +}; + +const onDidActiveInterpreterChangedEvent = new EventEmitter(); +export function reportActiveInterpreterChanged(e: ActiveEnvironmentChangeEvent): void { + onDidActiveInterpreterChangedEvent.fire({ id: normCasePath(e.path), path: e.path, resource: e.resource }); } -const onProgress = new EventEmitter(); const onEnvironmentsChanged = new EventEmitter(); const environmentsReference = new Map(); @@ -59,6 +62,10 @@ export class EnvironmentReference implements Environment { return this.internal.tools; } + get path() { + return this.internal.path; + } + updateEnv(newInternal: Environment) { this.internal = newInternal; } @@ -84,14 +91,6 @@ export function buildProposedApi( const configService = serviceContainer.get(IConfigurationService); const disposables = serviceContainer.get(IDisposableRegistry); disposables.push( - discoveryApi.onProgress((e) => { - if (e.stage === ProgressReportStage.discoveryStarted) { - onProgress.fire({ stateValue: RefreshStateValue.started }); - } - if (e.stage === ProgressReportStage.discoveryFinished) { - onProgress.fire({ stateValue: RefreshStateValue.finished }); - } - }), discoveryApi.onChanged((e) => { if (e.old) { if (e.new) { @@ -103,41 +102,35 @@ export function buildProposedApi( onEnvironmentsChanged.fire({ type: 'add', env: convertEnvInfoAndGetReference(e.new) }); } }), - onProgress, onEnvironmentsChanged, ); const proposed: ProposedExtensionAPI = { environment: { - getActiveEnvironmentSetting(resource?: Resource) { + getActiveEnvironmentId(resource?: Resource) { resource = resource && 'uri' in resource ? resource.uri : resource; const path = configService.getSettings(resource).pythonPath; - const id = path === 'python' ? 'defaultPython' : path; + const id = path === 'python' ? 'DEFAULT_PYTHON' : normCasePath(path); return { id, path }; }, - updateActiveEnvironmentSetting(env: string | Environment, resource?: Resource): Promise { - const path = typeof env !== 'string' ? env.id : env; + updateActiveEnvironmentId(env: Environment | EnvironmentId | string, resource?: Resource): Promise { + const path = typeof env !== 'string' ? env.path : env; resource = resource && 'uri' in resource ? resource.uri : resource; return interpreterPathService.update(resource, ConfigurationTarget.WorkspaceFolder, path); }, - onDidChangeActiveEnvironmentSetting: onDidActiveInterpreterChangedEvent.event, - resolveEnvironment: async (env: string | Environment) => { - let path = typeof env !== 'string' ? env.id : env; + onDidChangeActiveEnvironmentId: onDidActiveInterpreterChangedEvent.event, + resolveEnvironment: async (env: Environment | EnvironmentId | string) => { + let path = typeof env !== 'string' ? env.path : env; if (pathUtils.basename(path) === path) { // Value can be `python`, `python3`, `python3.9` etc. - // Note the following triggers autoselection if no interpreter is explictly - // selected, i.e the value is `python`. - // During shutdown we might not be able to get items out of the service container. - const pythonExecutionFactory = serviceContainer.tryGet( + // This case could eventually be handled by the internal discovery API itself. + const pythonExecutionFactory = serviceContainer.get( IPythonExecutionFactory, ); - const pythonExecutionService = pythonExecutionFactory - ? await pythonExecutionFactory.create({ pythonPath: path }) - : undefined; - const fullyQualifiedPath = pythonExecutionService - ? await pythonExecutionService.getExecutablePath().catch((ex) => { - traceError(ex); - }) - : undefined; + const pythonExecutionService = await pythonExecutionFactory.create({ pythonPath: path }); + const fullyQualifiedPath = await pythonExecutionService.getExecutablePath().catch((ex) => { + traceError('Cannot resolve full path', ex); + return undefined; + }); // Python path is invalid or python isn't installed. if (!fullyQualifiedPath) { return undefined; @@ -149,19 +142,6 @@ export function buildProposedApi( get environments(): Environment[] { return discoveryApi.getEnvs().map((e) => convertEnvInfoAndGetReference(e)); }, - get onDidChangeRefreshState() { - return onProgress.event; - }, - get refreshState(): RefreshState { - switch (discoveryApi.refreshState) { - case ProgressReportStage.discoveryStarted: - return { stateValue: RefreshStateValue.started }; - case ProgressReportStage.discoveryFinished: - return { stateValue: RefreshStateValue.finished }; - default: - return { stateValue: RefreshStateValue.started }; - } - }, async refreshEnvironments(options?: RefreshOptions) { await discoveryApi.triggerRefresh(undefined, { ifNotTriggerredAlready: !options?.forceRefresh, @@ -197,8 +177,10 @@ export function convertCompleteEnvInfo(env: PythonEnvInfo): ResolvedEnvironment if (env.type && !tool) { tool = 'Unknown'; } + const { path } = getEnvPath(env.executable.filename, env.location); const resolvedEnv: ResolvedEnvironment = { - id: getEnvPath(env.executable.filename, env.location).path, + path, + id: getEnvID(path), executable: { uri: Uri.file(env.executable.filename), bitness: convertArch(env.arch), @@ -212,8 +194,8 @@ export function convertCompleteEnvInfo(env: PythonEnvInfo): ResolvedEnvironment workspaceFolder: env.searchLocation, } : undefined, - version: version as PythonVersionInfo, - tools: tool ? [tool] : undefined, + version: version as ResolvedEnvironment['version'], + tools: tool ? [tool] : [], }; return resolvedEnv; } @@ -286,3 +268,7 @@ function convertArch(arch: Architecture) { return 'Unknown'; } } + +function getEnvID(path: string) { + return normCasePath(path); +} diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index 36bbdddf66e4..add6321a1e4e 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -15,7 +15,7 @@ interface EnvironmentAPI { * @param resource : Uri of a file or workspace folder. This is used to determine the env in a multi-root * scenario. If `undefined`, then the API returns what ever is set for the workspace. */ - getActiveEnvironmentSetting(resource?: Resource): ActiveEnvironmentSetting; + getActiveEnvironmentId(resource?: Resource): EnvironmentId; /** * Sets the active environment path for the python extension for the resource. Configuration target will always * be the workspace folder. @@ -23,31 +23,21 @@ interface EnvironmentAPI { * the environment itself. * @param resource : [optional] File or workspace to scope to a particular workspace folder. */ - updateActiveEnvironmentSetting(environment: Environment | string, resource?: Resource): Promise; + updateActiveEnvironmentId(environment: Environment | EnvironmentId | string, resource?: Resource): Promise; /** * This event is triggered when the active environment setting changes. */ - readonly onDidChangeActiveEnvironmentSetting: Event; + readonly onDidChangeActiveEnvironmentId: Event; /** * Carries environments found by the extension at the time of fetching the property. Note a refresh might be - * going on so this may not be the complete list. To wait on complete list use {@link refreshState()} and - * {@link onDidChangeRefreshState}. + * going on so this may not be the complete list. */ - readonly environments: Environment[] | undefined; + readonly environments: Environment[]; /** * 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. */ readonly onDidChangeEnvironments: Event; - /** - * Carries the current state in the refresh, i.e whether it started, finished, or any other relevant state. - */ - readonly refreshState: RefreshState; - /** - * Fires when a refresh state has been reached, i.e when it starts, finishes or any other relevant state. - * Tracks refresh progress for current list of known environments. - */ - readonly onDidChangeRefreshState: Event; /** * This API will trigger environment discovery, but only if it has not already happened in this VSCode session. * Useful for making sure env list is up-to-date when the caller needs it for the first time. @@ -65,34 +55,13 @@ interface EnvironmentAPI { * @param environment : Full path to environment folder or python executable for the environment. Can also pass * the environment id or the environment itself. */ - resolveEnvironment(environment: Environment | string): Promise; + resolveEnvironment(environment: Environment | EnvironmentId | string): Promise; /** - * @deprecated Use {@link getActiveEnvironmentSetting} instead. This will soon be removed. + * @deprecated Use {@link getActiveEnvironmentId} instead. This will soon be removed. */ getActiveEnvironmentPath(resource?: Resource): Promise; } -export type RefreshState = { - stateValue: RefreshStateValue; -}; - -/** - * Contains state values in the order they finish during a refresh cycle. - */ -export enum RefreshStateValue { - /** - * When a refresh is started. - */ - started = 0, - - // ...there can be more intimidatory states - - /** - * When a refresh is over. - */ - finished = 1, -} - export type RefreshOptions = { /** * Force trigger a refresh regardless of whether a refresh was already triggered. Note this can be expensive so @@ -104,11 +73,7 @@ export type RefreshOptions = { /** * Details about the environment. Note the environment folder, type and name never changes over time. */ -export type Environment = { - /** - * The unique ID of the environment. - */ - id: string; +export type Environment = EnvironmentId & { /** * Carries details about python executable. */ @@ -153,7 +118,7 @@ export type Environment = { /** * Carries Python version information known at this moment. */ - version: StandardVersionInfo & { + version: VersionInfo & { /** * Value of `sys.version` in sys module if known at this moment. */ @@ -162,53 +127,46 @@ export type Environment = { /** * Tools/plugins which created the environment or where it came from. First value in array corresponds * to the primary tool which manages the environment, which never changes over time. + * + * Array is empty if no tool is responsible for creating/managing the environment. Usually the case for + * global interpreters. */ - tools: EnvironmentTools[] | undefined; + tools: EnvironmentTools[]; }; -/** - * A new form of object `T` where no property can have the value of `undefined`. - */ -type MakeAllPropertiesNonNullable = { - [P in keyof T]: NonNullable; -}; -/** - * A new form of object `Type` where a specific property `Key` cannot be `undefined`. - */ -type MakePropertyNonNullable = Omit & - MakeAllPropertiesNonNullable>; - -type ExecutableInfo = MakePropertyNonNullable & - MakePropertyNonNullable; -export type PythonVersionInfo = MakeAllPropertiesNonNullable; - /** * Derived form of {@link Environment} where certain properties can no longer be `undefined`. Meant to represent an * {@link Environment} with complete information. */ -export interface ResolvedEnvironment { +export type ResolvedEnvironment = Environment & { /** - * The unique ID of the environment. + * Carries complete details about python executable. */ - id: string; - /** - * New form of {@link Environment.executable} object where properties `sysPrefix` and `bitness` cannot be - * `undefined`. - */ - executable: ExecutableInfo; - /** - * See {@link Environment.environment} for description. - */ - environment: Environment['environment']; - /** - * New form of {@link Environment.version} object where no properties can be `undefined`. - */ - version: PythonVersionInfo; + executable: { + /** + * Uri of the python interpreter/executable. Carries `undefined` in case an executable does not belong to + * the environment. + */ + uri: Uri | undefined; + /** + * Bitness of the environment. + */ + bitness: Architecture; + /** + * Value of `sys.prefix` in sys module. + */ + sysPrefix: string; + }; /** - * See {@link Environment.tools} for description. + * Carries complete Python version information. */ - tools: EnvironmentTools[] | undefined; -} + version: ResolvedVersionInfo & { + /** + * Value of `sys.version` in sys module if known at this moment. + */ + sysVersion: string; + }; +}; export type EnvironmentsChangedEvent = { env: Environment; @@ -220,7 +178,7 @@ export type EnvironmentsChangedEvent = { type: 'add' | 'remove' | 'update'; }; -export type ActiveEnvironmentSettingChangeEvent = ActiveEnvironmentSetting & { +export type ActiveEnvironmentIdChangeEvent = EnvironmentId & { /** * Workspace folder the environment changed for. */ @@ -232,7 +190,7 @@ export type ActiveEnvironmentSettingChangeEvent = ActiveEnvironmentSetting & { */ export type Resource = Uri | WorkspaceFolder; -export type ActiveEnvironmentSetting = { +export type EnvironmentId = { /** * The ID of the environment. */ @@ -290,13 +248,20 @@ export type PythonVersionRelease = { serial: number; }; -export type StandardVersionInfo = { +export type VersionInfo = { major: number | undefined; minor: number | undefined; micro: number | undefined; release: PythonVersionRelease | undefined; }; +export type ResolvedVersionInfo = { + major: number; + minor: number; + micro: number; + release: PythonVersionRelease; +}; + /** * @deprecated: Will be removed soon. */ diff --git a/src/test/interpreters/interpreterService.unit.test.ts b/src/test/interpreters/interpreterService.unit.test.ts index 4ed457866279..1bbf729e53b9 100644 --- a/src/test/interpreters/interpreterService.unit.test.ts +++ b/src/test/interpreters/interpreterService.unit.test.ts @@ -260,7 +260,6 @@ suite('Interpreters service', () => { await service._onConfigChanged(resource); interpreterDisplay.verifyAll(); sinon.assert.calledOnceWithExactly(reportActiveInterpreterChangedStub, { - id: 'current path', path: 'current path', resource: workspaceFolder, }); @@ -281,7 +280,6 @@ suite('Interpreters service', () => { await service._onConfigChanged(resource); interpreterDisplay.verifyAll(); sinon.assert.calledOnceWithExactly(reportActiveInterpreterChangedStub, { - id: 'current path', path: 'current path', resource: workspaceFolder, }); diff --git a/src/test/proposedApi.unit.test.ts b/src/test/proposedApi.unit.test.ts index 5c45bf24f7f0..19c1aba94721 100644 --- a/src/test/proposedApi.unit.test.ts +++ b/src/test/proposedApi.unit.test.ts @@ -3,10 +3,14 @@ import * as typemoq from 'typemoq'; import { assert, expect } from 'chai'; -import { Uri, Event, EventEmitter, ConfigurationTarget, WorkspaceFolder } from 'vscode'; +import { Uri, EventEmitter, ConfigurationTarget, WorkspaceFolder } from 'vscode'; import { cloneDeep } from 'lodash'; -import { IDisposableRegistry, IInterpreterPathService } from '../client/common/types'; -import { IInterpreterService } from '../client/interpreter/contracts'; +import { + IConfigurationService, + IDisposableRegistry, + IInterpreterPathService, + IPythonSettings, +} from '../client/common/types'; import { IServiceContainer } from '../client/ioc/types'; import { buildProposedApi, @@ -15,31 +19,24 @@ import { EnvironmentReference, reportActiveInterpreterChanged, } from '../client/proposedApi'; -import { - IDiscoveryAPI, - ProgressNotificationEvent, - ProgressReportStage, -} from '../client/pythonEnvironments/base/locator'; -import { PythonEnvironment } from '../client/pythonEnvironments/info'; +import { IDiscoveryAPI, ProgressNotificationEvent } from '../client/pythonEnvironments/base/locator'; import { buildEnvInfo } from '../client/pythonEnvironments/base/info/env'; import { sleep } from './core'; +import { PythonEnvKind, PythonEnvSource } from '../client/pythonEnvironments/base/info'; +import { Architecture } from '../client/common/utils/platform'; +import { PythonEnvCollectionChangedEvent } from '../client/pythonEnvironments/base/watcher'; import { ProposedExtensionAPI, - RefreshState, - RefreshStateValue, - ActiveEnvironmentSettingChangeEvent, + ActiveEnvironmentIdChangeEvent, EnvironmentsChangedEvent, } from '../client/proposedApiTypes'; -import { PythonEnvKind, PythonEnvSource } from '../client/pythonEnvironments/base/info'; -import { Architecture } from '../client/common/utils/platform'; -import { PythonEnvCollectionChangedEvent } from '../client/pythonEnvironments/base/watcher'; +import { normCasePath } from '../client/common/platform/fs-paths'; suite('Proposed Extension API', () => { let serviceContainer: typemoq.IMock; let discoverAPI: typemoq.IMock; let interpreterPathService: typemoq.IMock; - let interpreterService: typemoq.IMock; - let onDidExecutionEvent: Event; + let configService: typemoq.IMock; let onDidChangeRefreshState: EventEmitter; let onDidChangeEnvironments: EventEmitter; @@ -49,14 +46,12 @@ suite('Proposed Extension API', () => { serviceContainer = typemoq.Mock.ofType(); discoverAPI = typemoq.Mock.ofType(); interpreterPathService = typemoq.Mock.ofType(); - interpreterService = typemoq.Mock.ofType(); - onDidExecutionEvent = typemoq.Mock.ofType>().object; + configService = typemoq.Mock.ofType(); onDidChangeRefreshState = new EventEmitter(); onDidChangeEnvironments = new EventEmitter(); - interpreterService.setup((i) => i.onDidChangeInterpreterConfiguration).returns(() => onDidExecutionEvent); serviceContainer.setup((s) => s.get(IInterpreterPathService)).returns(() => interpreterPathService.object); - serviceContainer.setup((s) => s.get(IInterpreterService)).returns(() => interpreterService.object); + serviceContainer.setup((s) => s.get(IConfigurationService)).returns(() => configService.object); serviceContainer.setup((s) => s.get(IDisposableRegistry)).returns(() => []); discoverAPI.setup((d) => d.onProgress).returns(() => onDidChangeRefreshState.event); @@ -64,54 +59,44 @@ suite('Proposed Extension API', () => { proposed = buildProposedApi(discoverAPI.object, serviceContainer.object); }); - - test('Provides an event for tracking refresh progress', async () => { - const events: RefreshState[] = []; - proposed.environment.onDidChangeRefreshState((e) => { + test('Provide an event to track when active environment details change', async () => { + const events: ActiveEnvironmentIdChangeEvent[] = []; + proposed.environment.onDidChangeActiveEnvironmentId((e) => { events.push(e); }); - onDidChangeRefreshState.fire({ stage: ProgressReportStage.discoveryStarted }); - onDidChangeRefreshState.fire({ stage: ProgressReportStage.allPathsDiscovered }); - onDidChangeRefreshState.fire({ stage: ProgressReportStage.discoveryFinished }); + reportActiveInterpreterChanged({ path: 'path/to/environment', resource: undefined }); await sleep(1); - assert.deepEqual(events, [ - { stateValue: RefreshStateValue.started }, - { stateValue: RefreshStateValue.finished }, + { id: normCasePath('path/to/environment'), path: 'path/to/environment', resource: undefined }, ]); }); - test('Provide an event to track when active environment details change', async () => { - const events: ActiveEnvironmentSettingChangeEvent[] = []; - proposed.environment.onDidChangeActiveEnvironmentSetting((e) => { - events.push(e); - }); - reportActiveInterpreterChanged({ id: 'path/to/environment', path: 'path/to/environment', resource: undefined }); - await sleep(1); - assert.deepEqual(events, [{ id: 'path/to/environment', path: 'path/to/environment', resource: undefined }]); + test('getActiveEnvironmentId: No resource', () => { + const pythonPath = 'this/is/a/test/path'; + configService + .setup((c) => c.getSettings(undefined)) + .returns(() => (({ pythonPath } as unknown) as IPythonSettings)); + const actual = proposed.environment.getActiveEnvironmentId(); + assert.deepEqual(actual, { id: normCasePath(pythonPath), path: pythonPath }); }); - test('getActiveEnvironmentSetting: No resource', async () => { - const pythonPath = 'this/is/a/test/path'; - interpreterService - .setup((c) => c.getActiveInterpreter(undefined)) - .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); - const env = buildEnvInfo({ executable: pythonPath }); - discoverAPI.setup((d) => d.resolveEnv(pythonPath)).returns(() => Promise.resolve(env)); - const actual = await proposed.environment.fetchActiveEnvironment(); - assert.deepEqual((actual as EnvironmentReference).internal, convertCompleteEnvInfo(env)); + test('getActiveEnvironmentId: default python', () => { + const pythonPath = 'python'; + configService + .setup((c) => c.getSettings(undefined)) + .returns(() => (({ pythonPath } as unknown) as IPythonSettings)); + const actual = proposed.environment.getActiveEnvironmentId(); + assert.deepEqual(actual, { id: 'DEFAULT_PYTHON', path: pythonPath }); }); - test('getActiveEnvironmentSetting: With resource', async () => { + test('getActiveEnvironmentId: With resource', () => { const pythonPath = 'this/is/a/test/path'; const resource = Uri.file(__filename); - interpreterService - .setup((c) => c.getActiveInterpreter(resource)) - .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); - const env = buildEnvInfo({ executable: pythonPath }); - discoverAPI.setup((d) => d.resolveEnv(pythonPath)).returns(() => Promise.resolve(env)); - const actual = await proposed.environment.fetchActiveEnvironment(resource); - assert.deepEqual((actual as EnvironmentReference).internal, convertCompleteEnvInfo(env)); + configService + .setup((c) => c.getSettings(resource)) + .returns(() => (({ pythonPath } as unknown) as IPythonSettings)); + const actual = proposed.environment.getActiveEnvironmentId(resource); + assert.deepEqual(actual, { id: normCasePath(pythonPath), path: pythonPath }); }); test('resolveEnvironment: invalid environment (when passed as string)', async () => { @@ -318,30 +303,44 @@ suite('Proposed Extension API', () => { assert.deepEqual(eventValues, expectedEvents); }); - test('updateActiveEnvironment: no resource', async () => { + test('updateActiveEnvironmentId: no resource', async () => { interpreterPathService .setup((i) => i.update(undefined, ConfigurationTarget.WorkspaceFolder, 'this/is/a/test/python/path')) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); - await proposed.environment.updateActiveEnvironment('this/is/a/test/python/path'); + await proposed.environment.updateActiveEnvironmentId('this/is/a/test/python/path'); interpreterPathService.verifyAll(); }); - test('setActiveInterpreter: with uri', async () => { + test('updateActiveEnvironmentId: passed as Environment', async () => { + interpreterPathService + .setup((i) => i.update(undefined, ConfigurationTarget.WorkspaceFolder, 'this/is/a/test/python/path')) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + await proposed.environment.updateActiveEnvironmentId({ + id: normCasePath('this/is/a/test/python/path'), + path: 'this/is/a/test/python/path', + }); + + interpreterPathService.verifyAll(); + }); + + test('updateActiveEnvironmentId: with uri', async () => { const uri = Uri.parse('a'); interpreterPathService .setup((i) => i.update(uri, ConfigurationTarget.WorkspaceFolder, 'this/is/a/test/python/path')) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); - await proposed.environment.updateActiveEnvironment('this/is/a/test/python/path', uri); + await proposed.environment.updateActiveEnvironmentId('this/is/a/test/python/path', uri); interpreterPathService.verifyAll(); }); - test('setActiveInterpreter: with workspace folder', async () => { + test('updateActiveEnvironmentId: with workspace folder', async () => { const uri = Uri.parse('a'); interpreterPathService .setup((i) => i.update(uri, ConfigurationTarget.WorkspaceFolder, 'this/is/a/test/python/path')) @@ -353,7 +352,7 @@ suite('Proposed Extension API', () => { index: 0, }; - await proposed.environment.updateActiveEnvironment('this/is/a/test/python/path', workspace); + await proposed.environment.updateActiveEnvironmentId('this/is/a/test/python/path', workspace); interpreterPathService.verifyAll(); }); @@ -379,19 +378,4 @@ suite('Proposed Extension API', () => { discoverAPI.verifyAll(); }); - - test('Verify refreshStates are passed along appropriately', () => { - discoverAPI.setup((d) => d.refreshState).returns(() => ProgressReportStage.discoveryStarted); - assert.deepEqual(proposed.environment.refreshState, { - stateValue: RefreshStateValue.started, - }); - discoverAPI.setup((d) => d.refreshState).returns(() => ProgressReportStage.allPathsDiscovered); - assert.deepEqual(proposed.environment.refreshState, { - stateValue: RefreshStateValue.started, - }); - discoverAPI.setup((d) => d.refreshState).returns(() => ProgressReportStage.discoveryFinished); - assert.deepEqual(proposed.environment.refreshState, { - stateValue: RefreshStateValue.finished, - }); - }); }); From fcf788c67c2f2081b82ca839a05faacc669886d0 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 21 Sep 2022 14:12:56 -0700 Subject: [PATCH 43/60] Use getEnvId everywhere --- src/client/proposedApi.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index f117e1d0826a..967d97ba1158 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -34,7 +34,7 @@ type ActiveEnvironmentChangeEvent = { const onDidActiveInterpreterChangedEvent = new EventEmitter(); export function reportActiveInterpreterChanged(e: ActiveEnvironmentChangeEvent): void { - onDidActiveInterpreterChangedEvent.fire({ id: normCasePath(e.path), path: e.path, resource: e.resource }); + onDidActiveInterpreterChangedEvent.fire({ id: getEnvID(e.path), path: e.path, resource: e.resource }); } const onEnvironmentsChanged = new EventEmitter(); const environmentsReference = new Map(); @@ -109,7 +109,7 @@ export function buildProposedApi( getActiveEnvironmentId(resource?: Resource) { resource = resource && 'uri' in resource ? resource.uri : resource; const path = configService.getSettings(resource).pythonPath; - const id = path === 'python' ? 'DEFAULT_PYTHON' : normCasePath(path); + const id = path === 'python' ? 'DEFAULT_PYTHON' : getEnvID(path); return { id, path }; }, updateActiveEnvironmentId(env: Environment | EnvironmentId | string, resource?: Resource): Promise { From 8f877010cb6f29c20cb4e5cef424f373df2bca8d Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 21 Sep 2022 14:23:37 -0700 Subject: [PATCH 44/60] Update `package.json` --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 20a39d429eb9..1c7c96c35052 100644 --- a/package.json +++ b/package.json @@ -1805,6 +1805,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", From 310e401e26f9f42b3da3565f0b8947c50cdcdf52 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 21 Sep 2022 14:25:38 -0700 Subject: [PATCH 45/60] Update `package-lock` --- package-lock.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index be50eac6b1f9..ee55ee7b81a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "rxjs": "^6.5.4", "rxjs-compat": "^6.5.4", "semver": "^5.5.0", + "stack-trace": "0.0.10", "sudo-prompt": "^9.2.1", "tmp": "^0.0.33", "uint64be": "^3.0.0", @@ -12554,7 +12555,6 @@ "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", - "dev": true, "engines": { "node": "*" } @@ -25224,8 +25224,7 @@ "stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", - "dev": true + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" }, "static-extend": { "version": "0.1.2", From 0cb3047c2fa0b335733995b048974f4c79bf1305 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 21 Sep 2022 15:41:41 -0700 Subject: [PATCH 46/60] Install stack trace types --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 1c7c96c35052..dc4536d31841 100644 --- a/package.json +++ b/package.json @@ -1850,6 +1850,7 @@ "@types/xml2js": "^0.4.2", "@typescript-eslint/eslint-plugin": "^3.7.0", "@typescript-eslint/parser": "^3.7.0", + "@types/stack-trace": "0.0.29", "@vscode/telemetry-extractor": ">=1.9.8", "@vscode/test-electron": "^2.1.3", "chai": "^4.1.2", From eb829f77765528624f5b3d46efbca7b3ceb4c174 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 21 Sep 2022 15:43:08 -0700 Subject: [PATCH 47/60] Update `package-lock` --- package-lock.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/package-lock.json b/package-lock.json index ee55ee7b81a8..8bd10ba6f104 100644 --- a/package-lock.json +++ b/package-lock.json @@ -66,6 +66,7 @@ "@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", @@ -834,6 +835,12 @@ "integrity": "sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==", "dev": true }, + "node_modules/@types/stack-trace": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/stack-trace/-/stack-trace-0.0.29.tgz", + "integrity": "sha512-TgfOX+mGY/NyNxJLIbDWrO9DjGoVSW9+aB8H2yy1fy32jsvxijhmyJI9fDFgvz3YP4lvJaq9DzdR/M1bOgVc9g==", + "dev": true + }, "node_modules/@types/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.0.33.tgz", @@ -15989,6 +15996,12 @@ "integrity": "sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==", "dev": true }, + "@types/stack-trace": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/stack-trace/-/stack-trace-0.0.29.tgz", + "integrity": "sha512-TgfOX+mGY/NyNxJLIbDWrO9DjGoVSW9+aB8H2yy1fy32jsvxijhmyJI9fDFgvz3YP4lvJaq9DzdR/M1bOgVc9g==", + "dev": true + }, "@types/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.0.33.tgz", From 7e85709a3d766d8a3130b0dcbcd408a29b76c4bd Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Wed, 21 Sep 2022 15:52:24 -0700 Subject: [PATCH 48/60] Add API for determining extension from callstack --- .eslintignore | 1 - package.json | 4 +- src/client/common/application/extensions.ts | 66 ++++++++++++++++++++- src/client/common/types.ts | 5 ++ 4 files changed, 70 insertions(+), 6 deletions(-) diff --git a/.eslintignore b/.eslintignore index 2f759e89c516..7ba146c7d8e4 100644 --- a/.eslintignore +++ b/.eslintignore @@ -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 diff --git a/package.json b/package.json index dc4536d31841..a6bee88dd510 100644 --- a/package.json +++ b/package.json @@ -1842,15 +1842,15 @@ "@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", - "@types/stack-trace": "0.0.29", "@vscode/telemetry-extractor": ">=1.9.8", "@vscode/test-electron": "^2.1.3", "chai": "^4.1.2", diff --git a/src/client/common/application/extensions.ts b/src/client/common/application/extensions.ts index 359f31e15138..ee3c69e32506 100644 --- a/src/client/common/application/extensions.ts +++ b/src/client/common/application/extensions.ts @@ -1,14 +1,26 @@ -// 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 VS code has installed. Code borrowed from: + * https://github.com/microsoft/vscode-jupyter/blob/67fe33d072f11d6443cf232a06bed0ac5e24682c/src/platform/common/application/extensions.node.ts + */ @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[] { return extensions.all; } @@ -17,7 +29,55 @@ export class Extensions implements IExtensions { return extensions.onDidChange; } - public getExtension(extensionId: any) { + public getExtension(extensionId: string): Extension | undefined { return extensions.getExtension(extensionId); } + + 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' }; + } } diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 571a9a01b8a2..f8108d518aab 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -428,6 +428,11 @@ export interface IExtensions { * @return An extension or `undefined`. */ getExtension(extensionId: string): Extension | undefined; + + /** + * Determines which called into our extension code based on call stacks. + */ + determineExtensionFromCallStack(): Promise<{ extensionId: string; displayName: string }>; } export const IBrowserService = Symbol('IBrowserService'); From 6b4734d670fd6843d22b7802b95762ca895d8762 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 23 Sep 2022 15:24:03 -0700 Subject: [PATCH 49/60] Address some comments --- src/client/proposedApi.ts | 17 ++++++++++++----- src/client/proposedApiTypes.ts | 18 +++--------------- src/test/proposedApi.unit.test.ts | 12 ++++++------ 3 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index 967d97ba1158..0ee6fe9673d8 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -11,7 +11,7 @@ import { IServiceContainer } from './ioc/types'; import { ActiveEnvironmentIdChangeEvent, Environment, - EnvironmentsChangedEvent, + EnvironmentsChangeEvent, ProposedExtensionAPI, ResolvedEnvironment, RefreshOptions, @@ -20,7 +20,7 @@ import { EnvironmentTools, EnvironmentId, } from './proposedApiTypes'; -import { PythonEnvInfo, PythonEnvKind, PythonEnvType } from './pythonEnvironments/base/info'; +import { EnvPathType, PythonEnvInfo, PythonEnvKind, PythonEnvType } from './pythonEnvironments/base/info'; import { getEnvPath } from './pythonEnvironments/base/info/env'; import { IDiscoveryAPI } from './pythonEnvironments/base/locator'; import { IPythonExecutionFactory } from './common/process/types'; @@ -36,7 +36,7 @@ const onDidActiveInterpreterChangedEvent = new EventEmitter(); +const onEnvironmentsChanged = new EventEmitter(); const environmentsReference = new Map(); export class EnvironmentReference implements Environment { @@ -104,7 +104,14 @@ export function buildProposedApi( }), onEnvironmentsChanged, ); - const proposed: ProposedExtensionAPI = { + const proposed: ProposedExtensionAPI & { + environment: { + /** + * @deprecated Use {@link getActiveEnvironmentId} instead. This will soon be removed. + */ + getActiveEnvironmentPath(resource?: Resource): Promise; + }; + } = { environment: { getActiveEnvironmentId(resource?: Resource) { resource = resource && 'uri' in resource ? resource.uri : resource; @@ -139,7 +146,7 @@ export function buildProposedApi( } return resolveEnvironment(path, discoveryApi); }, - get environments(): Environment[] { + get all(): Environment[] { return discoveryApi.getEnvs().map((e) => convertEnvInfoAndGetReference(e)); }, async refreshEnvironments(options?: RefreshOptions) { diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index add6321a1e4e..d024a642c3e0 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -32,12 +32,12 @@ interface EnvironmentAPI { * Carries environments found by the extension at the time of fetching the property. Note a refresh might be * going on so this may not be the complete list. */ - readonly environments: Environment[]; + readonly all: readonly Environment[]; /** * 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. */ - readonly onDidChangeEnvironments: Event; + readonly onDidChangeEnvironments: Event; /** * This API will trigger environment discovery, but only if it has not already happened in this VSCode session. * Useful for making sure env list is up-to-date when the caller needs it for the first time. @@ -56,10 +56,6 @@ interface EnvironmentAPI { * the environment id or the environment itself. */ resolveEnvironment(environment: Environment | EnvironmentId | string): Promise; - /** - * @deprecated Use {@link getActiveEnvironmentId} instead. This will soon be removed. - */ - getActiveEnvironmentPath(resource?: Resource): Promise; } export type RefreshOptions = { @@ -168,7 +164,7 @@ export type ResolvedEnvironment = Environment & { }; }; -export type EnvironmentsChangedEvent = { +export type EnvironmentsChangeEvent = { env: Environment; /** * * "add": New environment is added. @@ -261,11 +257,3 @@ export type ResolvedVersionInfo = { micro: number; release: PythonVersionRelease; }; - -/** - * @deprecated: Will be removed soon. - */ -interface EnvPathType { - path: string; - pathType: 'envFolderPath' | 'interpreterPath'; -} diff --git a/src/test/proposedApi.unit.test.ts b/src/test/proposedApi.unit.test.ts index 19c1aba94721..bed7b293339e 100644 --- a/src/test/proposedApi.unit.test.ts +++ b/src/test/proposedApi.unit.test.ts @@ -28,7 +28,7 @@ import { PythonEnvCollectionChangedEvent } from '../client/pythonEnvironments/ba import { ProposedExtensionAPI, ActiveEnvironmentIdChangeEvent, - EnvironmentsChangedEvent, + EnvironmentsChangeEvent, } from '../client/proposedApiTypes'; import { normCasePath } from '../client/common/platform/fs-paths'; @@ -155,7 +155,7 @@ suite('Proposed Extension API', () => { test('environments: no pythons found', () => { discoverAPI.setup((d) => d.getEnvs()).returns(() => []); - const actual = proposed.environment.environments; + const actual = proposed.environment.all; expect(actual).to.be.deep.equal([]); }); @@ -205,7 +205,7 @@ suite('Proposed Extension API', () => { }, ]; discoverAPI.setup((d) => d.getEnvs()).returns(() => envs); - const actual = proposed.environment.environments; + const actual = proposed.environment.all; const actualEnvs = actual?.map((a) => (a as EnvironmentReference).internal); assert.deepEqual( actualEnvs?.sort((a, b) => a.id.localeCompare(b.id)), @@ -214,9 +214,9 @@ suite('Proposed Extension API', () => { }); test('Provide an event to track when list of environments change', async () => { - let events: EnvironmentsChangedEvent[] = []; - let eventValues: EnvironmentsChangedEvent[] = []; - let expectedEvents: EnvironmentsChangedEvent[] = []; + let events: EnvironmentsChangeEvent[] = []; + let eventValues: EnvironmentsChangeEvent[] = []; + let expectedEvents: EnvironmentsChangeEvent[] = []; proposed.environment.onDidChangeEnvironments((e) => { events.push(e); }); From ffc3ed2f573b6f0f277cdd7a5fce92480f59f465 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Fri, 23 Sep 2022 15:41:44 -0700 Subject: [PATCH 50/60] Make all properties readonly --- src/client/proposedApi.ts | 21 +++++++---- src/client/proposedApiTypes.ts | 66 +++++++++++++++++----------------- 2 files changed, 47 insertions(+), 40 deletions(-) diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index 0ee6fe9673d8..ac056704d266 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -39,6 +39,13 @@ export function reportActiveInterpreterChanged(e: ActiveEnvironmentChangeEvent): const onEnvironmentsChanged = new EventEmitter(); const environmentsReference = new Map(); +/** + * Make all properties in T mutable. + */ +type Mutable = { + -readonly [P in keyof T]: Mutable; +}; + export class EnvironmentReference implements Environment { readonly id: string; @@ -47,23 +54,23 @@ export class EnvironmentReference implements Environment { } get executable() { - return this.internal.executable; + return Object.freeze(this.internal.executable); } get environment() { - return this.internal.environment; + return Object.freeze(this.internal.environment); } get version() { - return this.internal.version; + return Object.freeze(this.internal.version); } get tools() { - return this.internal.tools; + return Object.freeze(this.internal.tools); } get path() { - return this.internal.path; + return Object.freeze(this.internal.path); } updateEnv(newInternal: Environment) { @@ -239,7 +246,7 @@ function convertKind(kind: PythonEnvKind): EnvironmentTools | undefined { } export function convertEnvInfo(env: PythonEnvInfo): Environment { - const convertedEnv = convertCompleteEnvInfo(env) as Environment; + const convertedEnv = convertCompleteEnvInfo(env) as Mutable; if (convertedEnv.executable.sysPrefix === '') { convertedEnv.executable.sysPrefix = undefined; } @@ -258,7 +265,7 @@ export function convertEnvInfo(env: PythonEnvInfo): Environment { if (convertedEnv.version.minor === -1) { convertedEnv.version.minor = undefined; } - return convertedEnv; + return convertedEnv as Environment; } function convertEnvInfoAndGetReference(env: PythonEnvInfo): Environment { diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index d024a642c3e0..06ed48e7ca34 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -73,52 +73,52 @@ export type Environment = EnvironmentId & { /** * Carries details about python executable. */ - executable: { + readonly executable: { /** * Uri of the python interpreter/executable. Carries `undefined` in case an executable does not belong to * the environment. */ - uri: Uri | undefined; + readonly uri: Uri | undefined; /** * Bitness if known at this moment. */ - bitness: Architecture | undefined; + readonly bitness: Architecture | undefined; /** * Value of `sys.prefix` in sys module if known at this moment. */ - sysPrefix: string | undefined; + readonly sysPrefix: string | undefined; }; /** * Carries details if it is an environment, otherwise `undefined` in case of global interpreters and others. */ - environment: + readonly environment: | { /** * Type of the environment. */ - type: EnvironmentType; + readonly type: EnvironmentType; /** * Name to the environment if any. */ - name: string | undefined; + readonly name: string | undefined; /** * Uri of the environment folder. */ - folderUri: Uri; + readonly folderUri: Uri; /** * Any specific workspace folder this environment is created for. */ - workspaceFolder: Uri | undefined; + readonly workspaceFolder: Uri | undefined; } | undefined; /** * Carries Python version information known at this moment. */ - version: VersionInfo & { + readonly version: VersionInfo & { /** * Value of `sys.version` in sys module if known at this moment. */ - sysVersion: string | undefined; + readonly sysVersion: string | undefined; }; /** * Tools/plugins which created the environment or where it came from. First value in array corresponds @@ -127,7 +127,7 @@ export type Environment = EnvironmentId & { * Array is empty if no tool is responsible for creating/managing the environment. Usually the case for * global interpreters. */ - tools: EnvironmentTools[]; + readonly tools: readonly EnvironmentTools[]; }; /** @@ -138,47 +138,47 @@ export type ResolvedEnvironment = Environment & { /** * Carries complete details about python executable. */ - executable: { + readonly executable: { /** * Uri of the python interpreter/executable. Carries `undefined` in case an executable does not belong to * the environment. */ - uri: Uri | undefined; + readonly uri: Uri | undefined; /** * Bitness of the environment. */ - bitness: Architecture; + readonly bitness: Architecture; /** * Value of `sys.prefix` in sys module. */ - sysPrefix: string; + readonly sysPrefix: string; }; /** * Carries complete Python version information. */ - version: ResolvedVersionInfo & { + readonly version: ResolvedVersionInfo & { /** * Value of `sys.version` in sys module if known at this moment. */ - sysVersion: string; + readonly sysVersion: string; }; }; export type EnvironmentsChangeEvent = { - env: Environment; + readonly env: Environment; /** * * "add": New environment is added. * * "remove": Existing environment in the list is removed. * * "update": New information found about existing environment. */ - type: 'add' | 'remove' | 'update'; + readonly type: 'add' | 'remove' | 'update'; }; export type ActiveEnvironmentIdChangeEvent = EnvironmentId & { /** * Workspace folder the environment changed for. */ - resource: WorkspaceFolder | undefined; + readonly resource: WorkspaceFolder | undefined; }; /** @@ -190,13 +190,13 @@ export type EnvironmentId = { /** * The ID of the environment. */ - id: string; + readonly id: string; /** * 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. */ - path: string; + readonly path: string; }; /** @@ -240,20 +240,20 @@ export type PythonReleaseLevel = 'alpha' | 'beta' | 'candidate' | 'final'; * Release information for a Python version. */ export type PythonVersionRelease = { - level: PythonReleaseLevel; - serial: number; + readonly level: PythonReleaseLevel; + readonly serial: number; }; export type VersionInfo = { - major: number | undefined; - minor: number | undefined; - micro: number | undefined; - release: PythonVersionRelease | undefined; + readonly major: number | undefined; + readonly minor: number | undefined; + readonly micro: number | undefined; + readonly release: PythonVersionRelease | undefined; }; export type ResolvedVersionInfo = { - major: number; - minor: number; - micro: number; - release: PythonVersionRelease; + readonly major: number; + readonly minor: number; + readonly micro: number; + readonly release: PythonVersionRelease; }; From 895aaf66615619cf7335af315b884ca8ef011efc Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 26 Sep 2022 12:28:57 -0700 Subject: [PATCH 51/60] Make environments API inline --- src/client/proposedApiTypes.ts | 101 +++++++++++++++++---------------- 1 file changed, 51 insertions(+), 50 deletions(-) diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index 06ed48e7ca34..48a1055f1897 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -6,56 +6,57 @@ import { CancellationToken, Event, Uri, WorkspaceFolder } from 'vscode'; // https://github.com/microsoft/vscode-python/wiki/Proposed-Environment-APIs export interface ProposedExtensionAPI { - environment: EnvironmentAPI; -} - -interface EnvironmentAPI { - /** - * Returns the environment configured by user in settings. - * @param resource : Uri of a file or workspace folder. This is used to determine the env in a multi-root - * scenario. If `undefined`, then the API returns what ever is set for the workspace. - */ - getActiveEnvironmentId(resource?: Resource): EnvironmentId; - /** - * Sets the active environment path for the python extension for the resource. Configuration target will always - * be the workspace folder. - * @param environment : Full path to environment folder or python executable for the environment. Can also pass - * the environment itself. - * @param resource : [optional] File or workspace to scope to a particular workspace folder. - */ - updateActiveEnvironmentId(environment: Environment | EnvironmentId | string, resource?: Resource): Promise; - /** - * This event is triggered when the active environment setting changes. - */ - readonly onDidChangeActiveEnvironmentId: Event; - /** - * Carries environments found by the extension at the time of fetching the property. Note a refresh might be - * going on so this may not be the complete list. - */ - readonly all: readonly Environment[]; - /** - * 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. - */ - readonly onDidChangeEnvironments: Event; - /** - * This API will trigger environment discovery, but only if it has not already happened in this VSCode session. - * Useful for making sure env list is up-to-date when the caller needs it for the first time. - * - * To force trigger a refresh regardless of whether a refresh was already triggered, see option - * {@link RefreshOptions.forceRefresh}. - * - * Note that if there is a refresh already going on then this returns the promise for that refresh. - * @param options Additional options for refresh. - * @param token A cancellation token that indicates a refresh is no longer needed. - */ - refreshEnvironments(options?: RefreshOptions, token?: CancellationToken): Promise; - /** - * Returns details for the given environment, or `undefined` if the env is invalid. - * @param environment : Full path to environment folder or python executable for the environment. Can also pass - * the environment id or the environment itself. - */ - resolveEnvironment(environment: Environment | EnvironmentId | string): Promise; + readonly environment: { + /** + * Returns the environment configured by user in settings. + * @param resource : Uri of a file or workspace folder. This is used to determine the env in a multi-root + * scenario. If `undefined`, then the API returns what ever is set for the workspace. + */ + getActiveEnvironmentId(resource?: Resource): EnvironmentId; + /** + * Sets the active environment path for the python extension for the resource. Configuration target will always + * be the workspace folder. + * @param environment : Full path to environment folder or python executable for the environment. Can also pass + * the environment itself. + * @param resource : [optional] File or workspace to scope to a particular workspace folder. + */ + updateActiveEnvironmentId( + environment: Environment | EnvironmentId | string, + resource?: Resource, + ): Promise; + /** + * This event is triggered when the active environment setting changes. + */ + readonly onDidChangeActiveEnvironmentId: Event; + /** + * Carries environments found by the extension at the time of fetching the property. Note this may not + * contain all environments in the system as a refresh might be going on. + */ + readonly all: readonly Environment[]; + /** + * 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. + */ + readonly onDidChangeEnvironments: Event; + /** + * This API will trigger environment discovery, but only if it has not already happened in this VSCode session. + * Useful for making sure env list is up-to-date when the caller needs it for the first time. + * + * To force trigger a refresh regardless of whether a refresh was already triggered, see option + * {@link RefreshOptions.forceRefresh}. + * + * Note that if there is a refresh already going on then this returns the promise for that refresh. + * @param options Additional options for refresh. + * @param token A cancellation token that indicates a refresh is no longer needed. + */ + refreshEnvironments(options?: RefreshOptions, token?: CancellationToken): Promise; + /** + * Returns details for the given environment, or `undefined` if the env is invalid. + * @param environment : Full path to environment folder or python executable for the environment. Can also pass + * the environment id or the environment itself. + */ + resolveEnvironment(environment: Environment | EnvironmentId | string): Promise; + }; } export type RefreshOptions = { From b5769ff5bc3448a9f6345bb6ad21660f5924e185 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 26 Sep 2022 12:37:44 -0700 Subject: [PATCH 52/60] Address internal team reviews --- src/client/proposedApi.ts | 8 ++++---- src/client/proposedApiTypes.ts | 12 +++++++----- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index ac056704d266..8689f5844d48 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -197,7 +197,7 @@ export function convertCompleteEnvInfo(env: PythonEnvInfo): ResolvedEnvironment id: getEnvID(path), executable: { uri: Uri.file(env.executable.filename), - bitness: convertArch(env.arch), + bitness: convertBitness(env.arch), sysPrefix: env.executable.sysPrefix, }, environment: env.type @@ -272,12 +272,12 @@ function convertEnvInfoAndGetReference(env: PythonEnvInfo): Environment { return getEnvReference(convertEnvInfo(env)); } -function convertArch(arch: Architecture) { +function convertBitness(arch: Architecture) { switch (arch) { case Architecture.x64: - return 'x64'; + return '64-bit'; case Architecture.x86: - return 'x86'; + return '32-bit'; default: return 'Unknown'; } diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index 48a1055f1897..3fb596f469cf 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -83,7 +83,7 @@ export type Environment = EnvironmentId & { /** * Bitness if known at this moment. */ - readonly bitness: Architecture | undefined; + readonly bitness: Bitness | undefined; /** * Value of `sys.prefix` in sys module if known at this moment. */ @@ -148,7 +148,7 @@ export type ResolvedEnvironment = Environment & { /** * Bitness of the environment. */ - readonly bitness: Architecture; + readonly bitness: Bitness; /** * Value of `sys.prefix` in sys module. */ @@ -206,7 +206,8 @@ export type EnvironmentId = { */ export type EnvironmentTools = KnownEnvironmentTools | string; /** - * Tools or plugins the Python extension is aware of. + * Tools or plugins the Python extension currently has built-in support for. Note this list is expected to shrink + * once tools have their own separate extensions. */ export type KnownEnvironmentTools = | 'Conda' @@ -223,14 +224,15 @@ export type KnownEnvironmentTools = */ export type EnvironmentType = KnownEnvironmentTypes | string; /** - * Environment types the Python extension is aware of. + * Environment types the Python extension currently has built-in support for. Note this list is expected to shrink + * once tools have their own separate extensions. */ export type KnownEnvironmentTypes = 'VirtualEnv' | 'Conda' | 'Unknown'; /** * Carries bitness for an environment. */ -export type Architecture = 'x86' | 'x64' | 'Unknown'; +export type Bitness = '64-bit' | '32-bit' | 'Unknown'; /** * The possible Python release levels. From 5fe5e947fe0a5601b61a62501868030e676e896a Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 26 Sep 2022 12:46:23 -0700 Subject: [PATCH 53/60] Modify KnownEnvType --- src/client/proposedApi.ts | 2 +- src/client/proposedApiTypes.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index 8689f5844d48..1dfdb5af0862 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -219,7 +219,7 @@ function convertEnvType(envType: PythonEnvType): EnvironmentType { return 'Conda'; } if (envType === PythonEnvType.Virtual) { - return 'VirtualEnv'; + return 'VirtualEnvironment'; } return 'Unknown'; } diff --git a/src/client/proposedApiTypes.ts b/src/client/proposedApiTypes.ts index 3fb596f469cf..89f9f1e4e5f4 100644 --- a/src/client/proposedApiTypes.ts +++ b/src/client/proposedApiTypes.ts @@ -224,10 +224,10 @@ export type KnownEnvironmentTools = */ export type EnvironmentType = KnownEnvironmentTypes | string; /** - * Environment types the Python extension currently has built-in support for. Note this list is expected to shrink - * once tools have their own separate extensions. + * Environment types the Python extension is aware of. Note this list is expected to shrink once tools have their + * own separate extensions, in which case they're expected to provide the type themselves. */ -export type KnownEnvironmentTypes = 'VirtualEnv' | 'Conda' | 'Unknown'; +export type KnownEnvironmentTypes = 'VirtualEnvironment' | 'Conda' | 'Unknown'; /** * Carries bitness for an environment. From e7f261e73efa89062b9ee38e73d34c4e15829e82 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 26 Sep 2022 13:26:40 -0700 Subject: [PATCH 54/60] Add telemetry for each API --- src/client/proposedApi.ts | 29 +++++++++++++++++++++++++++-- src/client/telemetry/constants.ts | 1 + src/client/telemetry/index.ts | 25 +++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index 1dfdb5af0862..099e9aba5598 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -4,7 +4,7 @@ import { ConfigurationTarget, EventEmitter, Uri, WorkspaceFolder } from 'vscode'; import * as pathUtils from 'path'; -import { IConfigurationService, IDisposableRegistry, IInterpreterPathService } from './common/types'; +import { IConfigurationService, IDisposableRegistry, IExtensions, IInterpreterPathService } from './common/types'; import { Architecture } from './common/utils/platform'; import { IInterpreterService } from './interpreter/contracts'; import { IServiceContainer } from './ioc/types'; @@ -26,6 +26,8 @@ import { IDiscoveryAPI } from './pythonEnvironments/base/locator'; import { IPythonExecutionFactory } from './common/process/types'; import { traceError } from './logging'; import { normCasePath } from './common/platform/fs-paths'; +import { sendTelemetryEvent } from './telemetry'; +import { EventName } from './telemetry/constants'; type ActiveEnvironmentChangeEvent = { resource: WorkspaceFolder | undefined; @@ -97,6 +99,19 @@ export function buildProposedApi( const interpreterService = serviceContainer.get(IInterpreterService); const configService = serviceContainer.get(IConfigurationService); const disposables = serviceContainer.get(IDisposableRegistry); + const extensions = serviceContainer.get(IExtensions); + function sendApiTelemetry(apiName: string) { + extensions + .determineExtensionFromCallStack() + .then((info) => + sendTelemetryEvent(EventName.PYTHON_ENVIRONMENTS_API, undefined, { + apiName, + extensionId: info.extensionId, + displayName: info.displayName, + }), + ) + .ignoreErrors(); + } disposables.push( discoveryApi.onChanged((e) => { if (e.old) { @@ -121,18 +136,24 @@ export function buildProposedApi( } = { environment: { getActiveEnvironmentId(resource?: Resource) { + sendApiTelemetry('getActiveEnvironmentId'); resource = resource && 'uri' in resource ? resource.uri : resource; const path = configService.getSettings(resource).pythonPath; const id = path === 'python' ? 'DEFAULT_PYTHON' : getEnvID(path); return { id, path }; }, updateActiveEnvironmentId(env: Environment | EnvironmentId | string, resource?: Resource): Promise { + sendApiTelemetry('updateActiveEnvironmentId'); const path = typeof env !== 'string' ? env.path : env; resource = resource && 'uri' in resource ? resource.uri : resource; return interpreterPathService.update(resource, ConfigurationTarget.WorkspaceFolder, path); }, - onDidChangeActiveEnvironmentId: onDidActiveInterpreterChangedEvent.event, + get onDidChangeActiveEnvironmentId() { + sendApiTelemetry('onDidChangeActiveEnvironmentId'); + return onDidActiveInterpreterChangedEvent.event; + }, resolveEnvironment: async (env: Environment | EnvironmentId | string) => { + sendApiTelemetry('resolveEnvironment'); let path = typeof env !== 'string' ? env.path : env; if (pathUtils.basename(path) === path) { // Value can be `python`, `python3`, `python3.9` etc. @@ -154,17 +175,21 @@ export function buildProposedApi( return resolveEnvironment(path, discoveryApi); }, get all(): Environment[] { + sendApiTelemetry('all'); return discoveryApi.getEnvs().map((e) => convertEnvInfoAndGetReference(e)); }, async refreshEnvironments(options?: RefreshOptions) { + sendApiTelemetry('refreshEnvironments'); await discoveryApi.triggerRefresh(undefined, { ifNotTriggerredAlready: !options?.forceRefresh, }); }, get onDidChangeEnvironments() { + sendApiTelemetry('onDidChangeEnvironments'); return onEnvironmentsChanged.event; }, async getActiveEnvironmentPath(resource?: Resource) { + sendApiTelemetry('getActiveEnvironmentPath'); resource = resource && 'uri' in resource ? resource.uri : resource; const env = await interpreterService.getActiveInterpreter(resource); if (!env) { diff --git a/src/client/telemetry/constants.ts b/src/client/telemetry/constants.ts index 2ab6c8a8a3ba..4a611fcf3e7f 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -19,6 +19,7 @@ export enum EventName { PYTHON_INTERPRETER = 'PYTHON_INTERPRETER', PYTHON_INSTALL_PACKAGE = 'PYTHON_INSTALL_PACKAGE', ENVIRONMENT_WITHOUT_PYTHON_SELECTED = 'ENVIRONMENT_WITHOUT_PYTHON_SELECTED', + PYTHON_ENVIRONMENTS_API = 'PYTHON_ENVIRONMENTS_API', PYTHON_INTERPRETER_DISCOVERY = 'PYTHON_INTERPRETER_DISCOVERY', PYTHON_INTERPRETER_AUTO_SELECTION = 'PYTHON_INTERPRETER_AUTO_SELECTION', PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES = 'PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES', diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 326b125df1cd..4c068e959ab6 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1107,6 +1107,31 @@ export interface IEventNamePropertyMapping { */ discovered: boolean; }; + + /** + * Telemetry event sent when another extension calls into python extension's environment API. Contains details + * of the other extension. + */ + /* __GDPR__ + "python_environments_api" : { + "extensionId" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": false , "owner": "karrtikr"}, + "displayName" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": false, "owner": "karrtikr" } + } + */ + [EventName.PYTHON_ENVIRONMENTS_API]: { + /** + * The ID of the extension calling the API. + */ + extensionId: string; + /** + * The name of the extension as displayed in marketplace. + */ + displayName: string; + /** + * The name of the API called. + */ + apiName: string; + }; /** * Telemetry event sent with details after updating the python interpreter */ From 3c081b76c39ad280e3ddc862cc5fd35e6dddc54e Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 26 Sep 2022 13:39:24 -0700 Subject: [PATCH 55/60] Add tests for telemetry --- src/test/proposedApi.unit.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/test/proposedApi.unit.test.ts b/src/test/proposedApi.unit.test.ts index bed7b293339e..816bf1051d25 100644 --- a/src/test/proposedApi.unit.test.ts +++ b/src/test/proposedApi.unit.test.ts @@ -8,6 +8,7 @@ import { cloneDeep } from 'lodash'; import { IConfigurationService, IDisposableRegistry, + IExtensions, IInterpreterPathService, IPythonSettings, } from '../client/common/types'; @@ -37,6 +38,7 @@ suite('Proposed Extension API', () => { let discoverAPI: typemoq.IMock; let interpreterPathService: typemoq.IMock; let configService: typemoq.IMock; + let extensions: typemoq.IMock; let onDidChangeRefreshState: EventEmitter; let onDidChangeEnvironments: EventEmitter; @@ -45,11 +47,17 @@ suite('Proposed Extension API', () => { setup(() => { serviceContainer = typemoq.Mock.ofType(); discoverAPI = typemoq.Mock.ofType(); + extensions = typemoq.Mock.ofType(); + extensions + .setup((e) => e.determineExtensionFromCallStack()) + .returns(() => Promise.resolve({ extensionId: 'id', displayName: 'displayName', apiName: 'apiName' })) + .verifiable(typemoq.Times.atLeastOnce()); interpreterPathService = typemoq.Mock.ofType(); configService = typemoq.Mock.ofType(); onDidChangeRefreshState = new EventEmitter(); onDidChangeEnvironments = new EventEmitter(); + serviceContainer.setup((s) => s.get(IExtensions)).returns(() => extensions.object); serviceContainer.setup((s) => s.get(IInterpreterPathService)).returns(() => interpreterPathService.object); serviceContainer.setup((s) => s.get(IConfigurationService)).returns(() => configService.object); serviceContainer.setup((s) => s.get(IDisposableRegistry)).returns(() => []); @@ -59,6 +67,12 @@ suite('Proposed Extension API', () => { proposed = buildProposedApi(discoverAPI.object, serviceContainer.object); }); + + teardown(() => { + // Verify each API method sends telemetry regarding who called the API. + extensions.verifyAll(); + }); + test('Provide an event to track when active environment details change', async () => { const events: ActiveEnvironmentIdChangeEvent[] = []; proposed.environment.onDidChangeActiveEnvironmentId((e) => { From db83f3e2dfc21a547131d01a1b7bdfc5cda8e27c Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 26 Sep 2022 13:44:50 -0700 Subject: [PATCH 56/60] Add comments --- src/client/common/application/extensions.ts | 7 +++++-- src/client/common/types.ts | 2 +- src/client/proposedApi.ts | 17 ++++++++++++++++- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/client/common/application/extensions.ts b/src/client/common/application/extensions.ts index ee3c69e32506..9d62e76d5da4 100644 --- a/src/client/common/application/extensions.ts +++ b/src/client/common/application/extensions.ts @@ -13,8 +13,7 @@ import { IFileSystem } from '../platform/types'; import { EXTENSION_ROOT_DIR } from '../constants'; /** - * Provides functions for tracking the list of extensions that VS code has installed. Code borrowed from: - * https://github.com/microsoft/vscode-jupyter/blob/67fe33d072f11d6443cf232a06bed0ac5e24682c/src/platform/common/application/extensions.node.ts + * Provides functions for tracking the list of extensions that VSCode has installed. */ @injectable() export class Extensions implements IExtensions { @@ -33,6 +32,10 @@ export class Extensions implements IExtensions { 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) { diff --git a/src/client/common/types.ts b/src/client/common/types.ts index f8108d518aab..66c91b13444d 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -430,7 +430,7 @@ export interface IExtensions { getExtension(extensionId: string): Extension | undefined; /** - * Determines which called into our extension code based on call stacks. + * Determines which extension called into our extension code based on call stacks. */ determineExtensionFromCallStack(): Promise<{ extensionId: string; displayName: string }>; } diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index 099e9aba5598..bc31d287d06b 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -2,7 +2,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { ConfigurationTarget, EventEmitter, Uri, WorkspaceFolder } from 'vscode'; +import { ConfigurationTarget, EventEmitter, Uri, WorkspaceFolder, Event } from 'vscode'; import * as pathUtils from 'path'; import { IConfigurationService, IDisposableRegistry, IExtensions, IInterpreterPathService } from './common/types'; import { Architecture } from './common/utils/platform'; @@ -29,14 +29,24 @@ import { normCasePath } from './common/platform/fs-paths'; import { sendTelemetryEvent } from './telemetry'; import { EventName } from './telemetry/constants'; +/** + * @deprecated Will be removed soon. + */ +interface ActiveEnvironmentChangedParams { + path: string; + resource?: Uri; +} + type ActiveEnvironmentChangeEvent = { resource: WorkspaceFolder | undefined; path: string; }; const onDidActiveInterpreterChangedEvent = new EventEmitter(); +const onDidActiveInterpreterChangedDeprecated = new EventEmitter(); export function reportActiveInterpreterChanged(e: ActiveEnvironmentChangeEvent): void { onDidActiveInterpreterChangedEvent.fire({ id: getEnvID(e.path), path: e.path, resource: e.resource }); + onDidActiveInterpreterChangedDeprecated.fire({ path: e.path, resource: e.resource?.uri }); } const onEnvironmentsChanged = new EventEmitter(); const environmentsReference = new Map(); @@ -132,6 +142,10 @@ export function buildProposedApi( * @deprecated Use {@link getActiveEnvironmentId} instead. This will soon be removed. */ getActiveEnvironmentPath(resource?: Resource): Promise; + /** + * @deprecated Use {@link onDidChangeActiveEnvironmentId} instead. This will soon be removed. + */ + onDidActiveEnvironmentChanged: Event; }; } = { environment: { @@ -197,6 +211,7 @@ export function buildProposedApi( } return getEnvPath(env.path, env.envPath); }, + onDidActiveEnvironmentChanged: onDidActiveInterpreterChangedDeprecated.event, }, }; return proposed; From c50f13d7f356e98a291632e182c521c813de3bbf Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 26 Sep 2022 15:57:44 -0700 Subject: [PATCH 57/60] Add support for deprecated API types --- src/client/deprecatedProposedApi.ts | 124 ++++++++++++++++++++ src/client/deprecatedProposedApiTypes.ts | 143 +++++++++++++++++++++++ src/client/proposedApi.ts | 78 +++++++------ 3 files changed, 309 insertions(+), 36 deletions(-) create mode 100644 src/client/deprecatedProposedApi.ts create mode 100644 src/client/deprecatedProposedApiTypes.ts diff --git a/src/client/deprecatedProposedApi.ts b/src/client/deprecatedProposedApi.ts new file mode 100644 index 000000000000..e65c869d49fe --- /dev/null +++ b/src/client/deprecatedProposedApi.ts @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { ConfigurationTarget, EventEmitter } from 'vscode'; +import { arePathsSame } from './common/platform/fs-paths'; +import { IInterpreterPathService, Resource } from './common/types'; +import { + EnvironmentsChangedParams, + ActiveEnvironmentChangedParams, + EnvironmentDetailsOptions, + EnvironmentDetails, + DeprecatedProposedAPI, +} from './deprecatedProposedApiTypes'; +import { IInterpreterService } from './interpreter/contracts'; +import { IServiceContainer } from './ioc/types'; +import { PythonEnvInfo } from './pythonEnvironments/base/info'; +import { getEnvPath } from './pythonEnvironments/base/info/env'; +import { GetRefreshEnvironmentsOptions, IDiscoveryAPI } from './pythonEnvironments/base/locator'; + +const onDidInterpretersChangedEvent = new EventEmitter(); +/** + * @deprecated Will be removed soon. + */ +export function reportInterpretersChanged(e: EnvironmentsChangedParams[]): void { + onDidInterpretersChangedEvent.fire(e); +} + +const onDidActiveInterpreterChangedEvent = new EventEmitter(); +/** + * @deprecated Will be removed soon. + */ +export function reportActiveInterpreterChangedDeprecated(e: ActiveEnvironmentChangedParams): void { + onDidActiveInterpreterChangedEvent.fire(e); +} + +function getVersionString(env: PythonEnvInfo): string[] { + const ver = [`${env.version.major}`, `${env.version.minor}`, `${env.version.micro}`]; + if (env.version.release) { + ver.push(`${env.version.release}`); + if (env.version.sysVersion) { + ver.push(`${env.version.release}`); + } + } + return ver; +} + +/** + * Returns whether the path provided matches the environment. + * @param path Path to environment folder or path to interpreter that uniquely identifies an environment. + * @param env Environment to match with. + */ +function isEnvSame(path: string, env: PythonEnvInfo) { + return arePathsSame(path, env.location) || arePathsSame(path, env.executable.filename); +} + +export function buildDeprecatedProposedApi( + discoveryApi: IDiscoveryAPI, + serviceContainer: IServiceContainer, +): DeprecatedProposedAPI { + const interpreterPathService = serviceContainer.get(IInterpreterPathService); + const interpreterService = serviceContainer.get(IInterpreterService); + + const proposed: DeprecatedProposedAPI = { + environment: { + async getExecutionDetails(resource?: Resource) { + const env = await interpreterService.getActiveInterpreter(resource); + return env ? { execCommand: [env.path] } : { execCommand: undefined }; + }, + async getActiveEnvironmentPath(resource?: Resource) { + const env = await interpreterService.getActiveInterpreter(resource); + if (!env) { + return undefined; + } + return getEnvPath(env.path, env.envPath); + }, + async getEnvironmentDetails( + path: string, + options?: EnvironmentDetailsOptions, + ): Promise { + let env: PythonEnvInfo | undefined; + if (options?.useCache) { + env = discoveryApi.getEnvs().find((v) => isEnvSame(path, v)); + } + if (!env) { + env = await discoveryApi.resolveEnv(path); + if (!env) { + return undefined; + } + } + return { + interpreterPath: env.executable.filename, + envFolderPath: env.location.length ? env.location : undefined, + version: getVersionString(env), + environmentType: [env.kind], + metadata: { + sysPrefix: env.executable.sysPrefix, + bitness: env.arch, + project: env.searchLocation, + }, + }; + }, + getEnvironmentPaths() { + const paths = discoveryApi.getEnvs().map((e) => getEnvPath(e.executable.filename, e.location)); + return Promise.resolve(paths); + }, + setActiveEnvironment(path: string, resource?: Resource): Promise { + return interpreterPathService.update(resource, ConfigurationTarget.WorkspaceFolder, path); + }, + async refreshEnvironment() { + await discoveryApi.triggerRefresh(); + const paths = discoveryApi.getEnvs().map((e) => getEnvPath(e.executable.filename, e.location)); + return Promise.resolve(paths); + }, + getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise | undefined { + return discoveryApi.getRefreshPromise(options); + }, + onDidChangeExecutionDetails: interpreterService.onDidChangeInterpreterConfiguration, + onDidEnvironmentsChanged: onDidInterpretersChangedEvent.event, + onDidActiveEnvironmentChanged: onDidActiveInterpreterChangedEvent.event, + onRefreshProgress: discoveryApi.onProgress, + }, + }; + return proposed; +} diff --git a/src/client/deprecatedProposedApiTypes.ts b/src/client/deprecatedProposedApiTypes.ts new file mode 100644 index 000000000000..3525fb54bff7 --- /dev/null +++ b/src/client/deprecatedProposedApiTypes.ts @@ -0,0 +1,143 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Uri, Event } from 'vscode'; +import { ProposedExtensionAPI, Resource } from './proposedApiTypes'; +import { PythonEnvKind, EnvPathType } from './pythonEnvironments/base/info'; +import { ProgressNotificationEvent, GetRefreshEnvironmentsOptions } from './pythonEnvironments/base/locator'; + +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; +} + +/** + * @deprecated Use {@link ProposedExtensionAPI} instead. + */ +export interface DeprecatedProposedAPI { + /** + * @deprecated Use {@link ProposedExtensionAPI.environment} instead. This will soon be removed. + */ + 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; + }>; + /** + * @deprecated Use {@link getActiveEnvironmentId} instead. This will soon be removed. + */ + 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; + /** + * @deprecated Use {@link ProposedExtensionAPI.environment} `onDidChangeActiveEnvironmentId` instead. This will soon be removed. + */ + onDidActiveEnvironmentChanged: Event; + }; +} diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index bc31d287d06b..77f6b0f169c4 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -2,11 +2,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { ConfigurationTarget, EventEmitter, Uri, WorkspaceFolder, Event } from 'vscode'; +import { ConfigurationTarget, EventEmitter, Uri, WorkspaceFolder } from 'vscode'; import * as pathUtils from 'path'; import { IConfigurationService, IDisposableRegistry, IExtensions, IInterpreterPathService } from './common/types'; import { Architecture } from './common/utils/platform'; -import { IInterpreterService } from './interpreter/contracts'; import { IServiceContainer } from './ioc/types'; import { ActiveEnvironmentIdChangeEvent, @@ -20,7 +19,7 @@ import { EnvironmentTools, EnvironmentId, } from './proposedApiTypes'; -import { EnvPathType, PythonEnvInfo, PythonEnvKind, PythonEnvType } from './pythonEnvironments/base/info'; +import { PythonEnvInfo, PythonEnvKind, PythonEnvType } from './pythonEnvironments/base/info'; import { getEnvPath } from './pythonEnvironments/base/info/env'; import { IDiscoveryAPI } from './pythonEnvironments/base/locator'; import { IPythonExecutionFactory } from './common/process/types'; @@ -28,14 +27,11 @@ import { traceError } from './logging'; import { normCasePath } from './common/platform/fs-paths'; import { sendTelemetryEvent } from './telemetry'; import { EventName } from './telemetry/constants'; - -/** - * @deprecated Will be removed soon. - */ -interface ActiveEnvironmentChangedParams { - path: string; - resource?: Uri; -} +import { + buildDeprecatedProposedApi, + reportActiveInterpreterChangedDeprecated, + reportInterpretersChanged, +} from './deprecatedProposedApi'; type ActiveEnvironmentChangeEvent = { resource: WorkspaceFolder | undefined; @@ -43,11 +39,11 @@ type ActiveEnvironmentChangeEvent = { }; const onDidActiveInterpreterChangedEvent = new EventEmitter(); -const onDidActiveInterpreterChangedDeprecated = new EventEmitter(); export function reportActiveInterpreterChanged(e: ActiveEnvironmentChangeEvent): void { onDidActiveInterpreterChangedEvent.fire({ id: getEnvID(e.path), path: e.path, resource: e.resource }); - onDidActiveInterpreterChangedDeprecated.fire({ path: e.path, resource: e.resource?.uri }); + reportActiveInterpreterChangedDeprecated({ path: e.path, resource: e.resource?.uri }); } + const onEnvironmentsChanged = new EventEmitter(); const environmentsReference = new Map(); @@ -106,7 +102,6 @@ export function buildProposedApi( serviceContainer: IServiceContainer, ): ProposedExtensionAPI { const interpreterPathService = serviceContainer.get(IInterpreterPathService); - const interpreterService = serviceContainer.get(IInterpreterService); const configService = serviceContainer.get(IConfigurationService); const disposables = serviceContainer.get(IDisposableRegistry); const extensions = serviceContainer.get(IExtensions); @@ -127,27 +122,47 @@ export function buildProposedApi( if (e.old) { if (e.new) { onEnvironmentsChanged.fire({ type: 'update', env: convertEnvInfoAndGetReference(e.new) }); + reportInterpretersChanged([ + { + path: getEnvPath(e.new.executable.filename, e.new.location).path, + type: 'update', + }, + ]); } else { onEnvironmentsChanged.fire({ type: 'remove', env: convertEnvInfoAndGetReference(e.old) }); + reportInterpretersChanged([ + { + path: getEnvPath(e.old.executable.filename, e.old.location).path, + type: 'remove', + }, + ]); } } else if (e.new) { onEnvironmentsChanged.fire({ type: 'add', env: convertEnvInfoAndGetReference(e.new) }); + reportInterpretersChanged([ + { + path: getEnvPath(e.new.executable.filename, e.new.location).path, + type: 'add', + }, + ]); } }), onEnvironmentsChanged, ); - const proposed: ProposedExtensionAPI & { - environment: { - /** - * @deprecated Use {@link getActiveEnvironmentId} instead. This will soon be removed. - */ - getActiveEnvironmentPath(resource?: Resource): Promise; - /** - * @deprecated Use {@link onDidChangeActiveEnvironmentId} instead. This will soon be removed. - */ - onDidActiveEnvironmentChanged: Event; - }; - } = { + + /** + * @deprecated Will be removed soon. Use {@link ProposedExtensionAPI} instead. + */ + let deprecatedProposedApi; + try { + deprecatedProposedApi = buildDeprecatedProposedApi(discoveryApi, serviceContainer); + } catch (ex) { + deprecatedProposedApi = { environment: {} }; + // Errors out only in case of testing. + // Also, these APIs no longer supported, no need to log error. + } + + const proposed: ProposedExtensionAPI = { environment: { getActiveEnvironmentId(resource?: Resource) { sendApiTelemetry('getActiveEnvironmentId'); @@ -202,16 +217,7 @@ export function buildProposedApi( sendApiTelemetry('onDidChangeEnvironments'); return onEnvironmentsChanged.event; }, - async getActiveEnvironmentPath(resource?: Resource) { - sendApiTelemetry('getActiveEnvironmentPath'); - resource = resource && 'uri' in resource ? resource.uri : resource; - const env = await interpreterService.getActiveInterpreter(resource); - if (!env) { - return undefined; - } - return getEnvPath(env.path, env.envPath); - }, - onDidActiveEnvironmentChanged: onDidActiveInterpreterChangedDeprecated.event, + ...deprecatedProposedApi.environment, }, }; return proposed; From f0763f843db18ac8f2cf232574892c891ef1bee2 Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 26 Sep 2022 16:01:51 -0700 Subject: [PATCH 58/60] Add logging and telemetry for deprecated APIs --- src/client/deprecatedProposedApi.ts | 45 +++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/src/client/deprecatedProposedApi.ts b/src/client/deprecatedProposedApi.ts index e65c869d49fe..bbe3917b2185 100644 --- a/src/client/deprecatedProposedApi.ts +++ b/src/client/deprecatedProposedApi.ts @@ -3,7 +3,7 @@ import { ConfigurationTarget, EventEmitter } from 'vscode'; import { arePathsSame } from './common/platform/fs-paths'; -import { IInterpreterPathService, Resource } from './common/types'; +import { IExtensions, IInterpreterPathService, Resource } from './common/types'; import { EnvironmentsChangedParams, ActiveEnvironmentChangedParams, @@ -16,6 +16,8 @@ import { IServiceContainer } from './ioc/types'; import { PythonEnvInfo } from './pythonEnvironments/base/info'; import { getEnvPath } from './pythonEnvironments/base/info/env'; import { GetRefreshEnvironmentsOptions, IDiscoveryAPI } from './pythonEnvironments/base/locator'; +import { sendTelemetryEvent } from './telemetry'; +import { EventName } from './telemetry/constants'; const onDidInterpretersChangedEvent = new EventEmitter(); /** @@ -59,14 +61,30 @@ export function buildDeprecatedProposedApi( ): DeprecatedProposedAPI { const interpreterPathService = serviceContainer.get(IInterpreterPathService); const interpreterService = serviceContainer.get(IInterpreterService); + const extensions = serviceContainer.get(IExtensions); + function sendApiTelemetry(apiName: string) { + console.warn('Extension is using deprecated python APIs which will be removed soon'); + extensions + .determineExtensionFromCallStack() + .then((info) => + sendTelemetryEvent(EventName.PYTHON_ENVIRONMENTS_API, undefined, { + apiName, + extensionId: info.extensionId, + displayName: info.displayName, + }), + ) + .ignoreErrors(); + } const proposed: DeprecatedProposedAPI = { environment: { async getExecutionDetails(resource?: Resource) { + sendApiTelemetry('getExecutionDetails'); const env = await interpreterService.getActiveInterpreter(resource); return env ? { execCommand: [env.path] } : { execCommand: undefined }; }, async getActiveEnvironmentPath(resource?: Resource) { + sendApiTelemetry('getActiveEnvironmentPath'); const env = await interpreterService.getActiveInterpreter(resource); if (!env) { return undefined; @@ -77,6 +95,7 @@ export function buildDeprecatedProposedApi( path: string, options?: EnvironmentDetailsOptions, ): Promise { + sendApiTelemetry('getEnvironmentDetails'); let env: PythonEnvInfo | undefined; if (options?.useCache) { env = discoveryApi.getEnvs().find((v) => isEnvSame(path, v)); @@ -100,24 +119,40 @@ export function buildDeprecatedProposedApi( }; }, getEnvironmentPaths() { + sendApiTelemetry('getEnvironmentPaths'); const paths = discoveryApi.getEnvs().map((e) => getEnvPath(e.executable.filename, e.location)); return Promise.resolve(paths); }, setActiveEnvironment(path: string, resource?: Resource): Promise { + sendApiTelemetry('setActiveEnvironment'); return interpreterPathService.update(resource, ConfigurationTarget.WorkspaceFolder, path); }, async refreshEnvironment() { + sendApiTelemetry('refreshEnvironment'); await discoveryApi.triggerRefresh(); const paths = discoveryApi.getEnvs().map((e) => getEnvPath(e.executable.filename, e.location)); return Promise.resolve(paths); }, getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise | undefined { + sendApiTelemetry('getRefreshPromise'); return discoveryApi.getRefreshPromise(options); }, - onDidChangeExecutionDetails: interpreterService.onDidChangeInterpreterConfiguration, - onDidEnvironmentsChanged: onDidInterpretersChangedEvent.event, - onDidActiveEnvironmentChanged: onDidActiveInterpreterChangedEvent.event, - onRefreshProgress: discoveryApi.onProgress, + get onDidChangeExecutionDetails() { + sendApiTelemetry('onDidChangeExecutionDetails'); + return interpreterService.onDidChangeInterpreterConfiguration; + }, + get onDidEnvironmentsChanged() { + sendApiTelemetry('onDidEnvironmentsChanged'); + return onDidInterpretersChangedEvent.event; + }, + get onDidActiveEnvironmentChanged() { + sendApiTelemetry('onDidActiveEnvironmentChanged'); + return onDidActiveInterpreterChangedEvent.event; + }, + get onRefreshProgress() { + sendApiTelemetry('onRefreshProgress'); + return discoveryApi.onProgress; + }, }, }; return proposed; From 7303339ca12b78a5ac116f491fc51fdfedbbc54f Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 26 Sep 2022 16:14:52 -0700 Subject: [PATCH 59/60] Add delay when sending telemetry --- src/client/deprecatedProposedApiTypes.ts | 3 +-- src/client/proposedApi.ts | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/client/deprecatedProposedApiTypes.ts b/src/client/deprecatedProposedApiTypes.ts index 3525fb54bff7..cf6c01f21219 100644 --- a/src/client/deprecatedProposedApiTypes.ts +++ b/src/client/deprecatedProposedApiTypes.ts @@ -1,9 +1,8 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. import { Uri, Event } from 'vscode'; -import { ProposedExtensionAPI, Resource } from './proposedApiTypes'; +import { Resource } from './proposedApiTypes'; import { PythonEnvKind, EnvPathType } from './pythonEnvironments/base/info'; import { ProgressNotificationEvent, GetRefreshEnvironmentsOptions } from './pythonEnvironments/base/locator'; diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index 77f6b0f169c4..31e88750541e 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -182,7 +182,6 @@ export function buildProposedApi( return onDidActiveInterpreterChangedEvent.event; }, resolveEnvironment: async (env: Environment | EnvironmentId | string) => { - sendApiTelemetry('resolveEnvironment'); let path = typeof env !== 'string' ? env.path : env; if (pathUtils.basename(path) === path) { // Value can be `python`, `python3`, `python3.9` etc. @@ -201,6 +200,7 @@ export function buildProposedApi( } path = fullyQualifiedPath; } + sendApiTelemetry('resolveEnvironment'); return resolveEnvironment(path, discoveryApi); }, get all(): Environment[] { @@ -208,10 +208,10 @@ export function buildProposedApi( return discoveryApi.getEnvs().map((e) => convertEnvInfoAndGetReference(e)); }, async refreshEnvironments(options?: RefreshOptions) { - sendApiTelemetry('refreshEnvironments'); await discoveryApi.triggerRefresh(undefined, { ifNotTriggerredAlready: !options?.forceRefresh, }); + sendApiTelemetry('refreshEnvironments'); }, get onDidChangeEnvironments() { sendApiTelemetry('onDidChangeEnvironments'); From fff4e1155c3749a973868742b1e2d9982cf922ec Mon Sep 17 00:00:00 2001 From: Kartik Raj Date: Mon, 26 Sep 2022 16:29:15 -0700 Subject: [PATCH 60/60] Fix tests --- src/client/deprecatedProposedApi.ts | 14 ++++++++------ src/client/proposedApi.ts | 10 +++++----- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/client/deprecatedProposedApi.ts b/src/client/deprecatedProposedApi.ts index bbe3917b2185..84340772901a 100644 --- a/src/client/deprecatedProposedApi.ts +++ b/src/client/deprecatedProposedApi.ts @@ -62,8 +62,10 @@ export function buildDeprecatedProposedApi( const interpreterPathService = serviceContainer.get(IInterpreterPathService); const interpreterService = serviceContainer.get(IInterpreterService); const extensions = serviceContainer.get(IExtensions); - function sendApiTelemetry(apiName: string) { - console.warn('Extension is using deprecated python APIs which will be removed soon'); + function sendApiTelemetry(apiName: string, warnLog = true) { + if (warnLog) { + console.warn('Extension is using deprecated python APIs which will be removed soon'); + } extensions .determineExtensionFromCallStack() .then((info) => @@ -138,19 +140,19 @@ export function buildDeprecatedProposedApi( return discoveryApi.getRefreshPromise(options); }, get onDidChangeExecutionDetails() { - sendApiTelemetry('onDidChangeExecutionDetails'); + sendApiTelemetry('onDidChangeExecutionDetails', false); return interpreterService.onDidChangeInterpreterConfiguration; }, get onDidEnvironmentsChanged() { - sendApiTelemetry('onDidEnvironmentsChanged'); + sendApiTelemetry('onDidEnvironmentsChanged', false); return onDidInterpretersChangedEvent.event; }, get onDidActiveEnvironmentChanged() { - sendApiTelemetry('onDidActiveEnvironmentChanged'); + sendApiTelemetry('onDidActiveEnvironmentChanged', false); return onDidActiveInterpreterChangedEvent.event; }, get onRefreshProgress() { - sendApiTelemetry('onRefreshProgress'); + sendApiTelemetry('onRefreshProgress', false); return discoveryApi.onProgress; }, }, diff --git a/src/client/proposedApi.ts b/src/client/proposedApi.ts index 31e88750541e..e85c4009d2c9 100644 --- a/src/client/proposedApi.ts +++ b/src/client/proposedApi.ts @@ -151,13 +151,13 @@ export function buildProposedApi( ); /** - * @deprecated Will be removed soon. Use {@link ProposedExtensionAPI} instead. + * @deprecated Will be removed soon. Use {@link ProposedExtensionAPI.environment} instead. */ - let deprecatedProposedApi; + let deprecatedEnvironmentsApi; try { - deprecatedProposedApi = buildDeprecatedProposedApi(discoveryApi, serviceContainer); + deprecatedEnvironmentsApi = { ...buildDeprecatedProposedApi(discoveryApi, serviceContainer).environment }; } catch (ex) { - deprecatedProposedApi = { environment: {} }; + deprecatedEnvironmentsApi = {}; // Errors out only in case of testing. // Also, these APIs no longer supported, no need to log error. } @@ -217,7 +217,7 @@ export function buildProposedApi( sendApiTelemetry('onDidChangeEnvironments'); return onEnvironmentsChanged.event; }, - ...deprecatedProposedApi.environment, + ...deprecatedEnvironmentsApi, }, }; return proposed;