diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0fc789d8c69b..42d94e49041c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -257,6 +257,13 @@ jobs: - name: Install test requirements run: python -m pip install --upgrade -r build/test-requirements.txt + - name: Rust Tool Chain setup + uses: dtolnay/rust-toolchain@stable + + - name: Build Native Binaries + run: nox --session native_build + shell: bash + - name: Install functional test requirements run: python -m pip install --upgrade -r ./build/functional-test-requirements.txt if: matrix.test-suite == 'functional' diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 34c8c6cc8e79..063857894210 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -253,6 +253,13 @@ jobs: - name: Install test requirements run: python -m pip install --upgrade -r build/test-requirements.txt + - name: Rust Tool Chain setup + uses: dtolnay/rust-toolchain@stable + + - name: Build Native Binaries + run: nox --session native_build + shell: bash + - name: Install functional test requirements run: python -m pip install --upgrade -r ./build/functional-test-requirements.txt if: matrix.test-suite == 'functional' @@ -512,6 +519,17 @@ jobs: requirements-file: './python_files/jedilsp_requirements/requirements.txt' options: '-t ./python_files/lib/jedilsp --implementation py' + - name: Install build pre-requisite + run: python -m pip install wheel nox + shell: bash + + - name: Rust Tool Chain setup + uses: dtolnay/rust-toolchain@stable + + - name: Build Native Binaries + run: nox --session native_build + shell: bash + - name: Install test requirements run: python -m pip install --upgrade -r build/test-requirements.txt diff --git a/src/client/pythonEnvironments/base/locators/common/nativePythonFinder.ts b/src/client/pythonEnvironments/base/locators/common/nativePythonFinder.ts index 9dc440a7a88f..e811badce553 100644 --- a/src/client/pythonEnvironments/base/locators/common/nativePythonFinder.ts +++ b/src/client/pythonEnvironments/base/locators/common/nativePythonFinder.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { Disposable, EventEmitter, Event, Uri, LogOutputChannel } from 'vscode'; +import { Disposable, EventEmitter, Event, Uri } from 'vscode'; import * as ch from 'child_process'; import * as path from 'path'; import * as rpc from 'vscode-jsonrpc/node'; @@ -16,9 +16,8 @@ import { CONDAPATH_SETTING_KEY } from '../../../common/environmentManagers/conda import { VENVFOLDERS_SETTING_KEY, VENVPATH_SETTING_KEY } from '../lowLevel/customVirtualEnvLocator'; import { getUserHomeDir } from '../../../../common/utils/platform'; import { createLogOutputChannel } from '../../../../common/vscodeApis/windowApis'; -import { PythonEnvKind } from '../../info'; import { sendNativeTelemetry, NativePythonTelemetry } from './nativePythonTelemetry'; -import { traceError } from '../../../../logging'; +import { NativePythonEnvironmentKind } from './nativePythonUtils'; const untildify = require('untildify'); @@ -30,7 +29,7 @@ export interface NativeEnvInfo { displayName?: string; name?: string; executable?: string; - kind?: PythonEnvironmentKind; + kind?: NativePythonEnvironmentKind; version?: string; prefix?: string; manager?: NativeEnvManagerInfo; @@ -42,32 +41,13 @@ export interface NativeEnvInfo { symlinks?: string[]; } -export enum PythonEnvironmentKind { - Conda = 'Conda', - Homebrew = 'Homebrew', - Pyenv = 'Pyenv', - GlobalPaths = 'GlobalPaths', - PyenvVirtualEnv = 'PyenvVirtualEnv', - Pipenv = 'Pipenv', - Poetry = 'Poetry', - MacPythonOrg = 'MacPythonOrg', - MacCommandLineTools = 'MacCommandLineTools', - LinuxGlobal = 'LinuxGlobal', - MacXCode = 'MacXCode', - Venv = 'Venv', - VirtualEnv = 'VirtualEnv', - VirtualEnvWrapper = 'VirtualEnvWrapper', - WindowsStore = 'WindowsStore', - WindowsRegistry = 'WindowsRegistry', -} - export interface NativeEnvManagerInfo { tool: string; executable: string; version?: string; } -export function isNativeInfoEnvironment(info: NativeEnvInfo | NativeEnvManagerInfo): info is NativeEnvInfo { +export function isNativeEnvInfo(info: NativeEnvInfo | NativeEnvManagerInfo): info is NativeEnvInfo { if ((info as NativeEnvManagerInfo).tool) { return false; } @@ -92,63 +72,26 @@ export interface NativePythonFinder extends Disposable { * * If a Uri is provided, then it will search for python environments in that location (ignoring workspaces). * Uri can be a file or a folder. - * If a PythonEnvironmentKind is provided, then it will search for python environments of that kind (ignoring workspaces). + * If a NativePythonEnvironmentKind is provided, then it will search for python environments of that kind (ignoring workspaces). */ - refresh(options?: PythonEnvironmentKind | Uri[]): AsyncIterable; + refresh(options?: NativePythonEnvironmentKind | Uri[]): AsyncIterable; /** * Will spawn the provided Python executable and return information about the environment. * @param executable */ resolve(executable: string): Promise; - categoryToKind(category?: PythonEnvironmentKind): PythonEnvKind; /** * Used only for telemetry. */ getCondaInfo(): Promise; } -const mapping = new Map([ - [PythonEnvironmentKind.Conda, PythonEnvKind.Conda], - [PythonEnvironmentKind.GlobalPaths, PythonEnvKind.OtherGlobal], - [PythonEnvironmentKind.Pyenv, PythonEnvKind.Pyenv], - [PythonEnvironmentKind.PyenvVirtualEnv, PythonEnvKind.Pyenv], - [PythonEnvironmentKind.Pipenv, PythonEnvKind.Pipenv], - [PythonEnvironmentKind.Poetry, PythonEnvKind.Poetry], - [PythonEnvironmentKind.VirtualEnv, PythonEnvKind.VirtualEnv], - [PythonEnvironmentKind.VirtualEnvWrapper, PythonEnvKind.VirtualEnvWrapper], - [PythonEnvironmentKind.Venv, PythonEnvKind.Venv], - [PythonEnvironmentKind.WindowsRegistry, PythonEnvKind.System], - [PythonEnvironmentKind.WindowsStore, PythonEnvKind.MicrosoftStore], - [PythonEnvironmentKind.Homebrew, PythonEnvKind.System], - [PythonEnvironmentKind.LinuxGlobal, PythonEnvKind.System], - [PythonEnvironmentKind.MacCommandLineTools, PythonEnvKind.System], - [PythonEnvironmentKind.MacPythonOrg, PythonEnvKind.System], - [PythonEnvironmentKind.MacXCode, PythonEnvKind.System], -]); - -export function categoryToKind(category?: PythonEnvironmentKind, logger?: LogOutputChannel): PythonEnvKind { - if (!category) { - return PythonEnvKind.Unknown; - } - const kind = mapping.get(category); - if (kind) { - return kind; - } - - if (logger) { - logger.error(`Unknown Python Environment category '${category}' from Native Locator.`); - } else { - traceError(`Unknown Python Environment category '${category}' from Native Locator.`); - } - return PythonEnvKind.Unknown; -} - interface NativeLog { level: string; message: string; } -class NativeGlobalPythonFinderImpl extends DisposableBase implements NativePythonFinder { +class NativePythonFinderImpl extends DisposableBase implements NativePythonFinder { private readonly connection: rpc.MessageConnection; private firstRefreshResults: undefined | (() => AsyncGenerator); @@ -171,11 +114,7 @@ class NativeGlobalPythonFinderImpl extends DisposableBase implements NativePytho return environment; } - categoryToKind(category?: PythonEnvironmentKind): PythonEnvKind { - return categoryToKind(category, this.outputChannel); - } - - async *refresh(options?: PythonEnvironmentKind | Uri[]): AsyncIterable { + async *refresh(options?: NativePythonEnvironmentKind | Uri[]): AsyncIterable { if (this.firstRefreshResults) { // If this is the first time we are refreshing, // Then get the results from the first refresh. @@ -321,7 +260,7 @@ class NativeGlobalPythonFinderImpl extends DisposableBase implements NativePytho } private doRefresh( - options?: PythonEnvironmentKind | Uri[], + options?: NativePythonEnvironmentKind | Uri[], ): { completed: Promise; discovered: Event } { const disposable = this._register(new DisposableStore()); const discovered = disposable.add(new EventEmitter()); @@ -383,7 +322,7 @@ class NativeGlobalPythonFinderImpl extends DisposableBase implements NativePytho ); type RefreshOptions = { - searchKind?: PythonEnvironmentKind; + searchKind?: NativePythonEnvironmentKind; searchPaths?: string[]; }; @@ -422,6 +361,7 @@ class NativeGlobalPythonFinderImpl extends DisposableBase implements NativePytho environmentDirectories: getCustomVirtualEnvDirs(), condaExecutable: getPythonSettingAndUntildify(CONDAPATH_SETTING_KEY), poetryExecutable: getPythonSettingAndUntildify('poetryPath'), + // We don't use pipenvPath as it is not used for discovery }; // No need to send a configuration request, is there are no changes. if (JSON.stringify(options) === JSON.stringify(this.lastConfiguration || {})) { @@ -483,7 +423,7 @@ function getPythonSettingAndUntildify(name: string, scope?: Uri): T | undefin let _finder: NativePythonFinder | undefined; export function getNativePythonFinder(): NativePythonFinder { if (!_finder) { - _finder = new NativeGlobalPythonFinderImpl(); + _finder = new NativePythonFinderImpl(); } return _finder; } diff --git a/src/client/pythonEnvironments/base/locators/common/nativePythonUtils.ts b/src/client/pythonEnvironments/base/locators/common/nativePythonUtils.ts new file mode 100644 index 000000000000..f840ce9a41ec --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/common/nativePythonUtils.ts @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { LogOutputChannel } from 'vscode'; +import { PythonEnvKind } from '../../info'; +import { traceError } from '../../../../logging'; + +export enum NativePythonEnvironmentKind { + Conda = 'Conda', + Homebrew = 'Homebrew', + Pyenv = 'Pyenv', + GlobalPaths = 'GlobalPaths', + PyenvVirtualEnv = 'PyenvVirtualEnv', + Pipenv = 'Pipenv', + Poetry = 'Poetry', + MacPythonOrg = 'MacPythonOrg', + MacCommandLineTools = 'MacCommandLineTools', + LinuxGlobal = 'LinuxGlobal', + MacXCode = 'MacXCode', + Venv = 'Venv', + VirtualEnv = 'VirtualEnv', + VirtualEnvWrapper = 'VirtualEnvWrapper', + WindowsStore = 'WindowsStore', + WindowsRegistry = 'WindowsRegistry', +} + +const mapping = new Map([ + [NativePythonEnvironmentKind.Conda, PythonEnvKind.Conda], + [NativePythonEnvironmentKind.GlobalPaths, PythonEnvKind.OtherGlobal], + [NativePythonEnvironmentKind.Pyenv, PythonEnvKind.Pyenv], + [NativePythonEnvironmentKind.PyenvVirtualEnv, PythonEnvKind.Pyenv], + [NativePythonEnvironmentKind.Pipenv, PythonEnvKind.Pipenv], + [NativePythonEnvironmentKind.Poetry, PythonEnvKind.Poetry], + [NativePythonEnvironmentKind.VirtualEnv, PythonEnvKind.VirtualEnv], + [NativePythonEnvironmentKind.VirtualEnvWrapper, PythonEnvKind.VirtualEnvWrapper], + [NativePythonEnvironmentKind.Venv, PythonEnvKind.Venv], + [NativePythonEnvironmentKind.WindowsRegistry, PythonEnvKind.System], + [NativePythonEnvironmentKind.WindowsStore, PythonEnvKind.MicrosoftStore], + [NativePythonEnvironmentKind.Homebrew, PythonEnvKind.System], + [NativePythonEnvironmentKind.LinuxGlobal, PythonEnvKind.System], + [NativePythonEnvironmentKind.MacCommandLineTools, PythonEnvKind.System], + [NativePythonEnvironmentKind.MacPythonOrg, PythonEnvKind.System], + [NativePythonEnvironmentKind.MacXCode, PythonEnvKind.System], +]); + +export function categoryToKind(category?: NativePythonEnvironmentKind, logger?: LogOutputChannel): PythonEnvKind { + if (!category) { + return PythonEnvKind.Unknown; + } + const kind = mapping.get(category); + if (kind) { + return kind; + } + + if (logger) { + logger.error(`Unknown Python Environment category '${category}' from Native Locator.`); + } else { + traceError(`Unknown Python Environment category '${category}' from Native Locator.`); + } + return PythonEnvKind.Unknown; +} diff --git a/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts b/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts index 1c56525f516a..5f1fc7c3bf05 100644 --- a/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts +++ b/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts @@ -27,7 +27,7 @@ import { PythonEnvCollectionChangedEvent, PythonEnvsWatcher } from '../../watche import { IEnvsCollectionCache } from './envsCollectionCache'; import { getNativePythonFinder, - isNativeInfoEnvironment, + isNativeEnvInfo, NativeEnvInfo, NativePythonFinder, } from '../common/nativePythonFinder'; @@ -37,6 +37,7 @@ import { parseVersion } from '../../info/pythonVersion'; import { Conda, CONDAPATH_SETTING_KEY, isCondaEnvironment } from '../../../common/environmentManagers/conda'; import { getConfiguration } from '../../../../common/vscodeApis/workspaceApis'; import { getUserHomeDir } from '../../../../common/utils/platform'; +import { categoryToKind } from '../common/nativePythonUtils'; /** * A service which maintains the collection of known environments. @@ -299,7 +300,7 @@ export class EnvsCollectionService extends PythonEnvsWatcher(); const nativeStopWatch = new StopWatch(); for await (const data of this.nativeFinder.refresh()) { - if (isNativeInfoEnvironment(data)) { + if (isNativeEnvInfo(data)) { nativeEnvs.push(data); if (data.executable) { // Lowercase for purposes of comparison (safe). @@ -333,9 +334,7 @@ export class EnvsCollectionService extends PythonEnvsWatcher this.nativeFinder.categoryToKind(e.kind) === PythonEnvKind.Conda, - ); + const nativeCondaEnvs = nativeEnvs.filter((e) => categoryToKind(e.kind) === PythonEnvKind.Conda); const condaTelemetry = await getCondaTelemetry(this.nativeFinder, nativeCondaEnvs, nativeEnvs); const prefixesSeenAlready = new Set(); await Promise.all( @@ -480,47 +479,31 @@ export class EnvsCollectionService extends PythonEnvsWatcher e.executable === undefined).length; - const nativeCustomEnvs = nativeEnvs.filter( - (e) => this.nativeFinder.categoryToKind(e.kind) === PythonEnvKind.Custom, - ).length; + const nativeCustomEnvs = nativeEnvs.filter((e) => categoryToKind(e.kind) === PythonEnvKind.Custom).length; const nativeMicrosoftStoreEnvs = nativeEnvs.filter( - (e) => this.nativeFinder.categoryToKind(e.kind) === PythonEnvKind.MicrosoftStore, - ).length; - const nativeOtherGlobalEnvs = nativeEnvs.filter( - (e) => this.nativeFinder.categoryToKind(e.kind) === PythonEnvKind.OtherGlobal, - ).length; - const nativeOtherVirtualEnvs = nativeEnvs.filter( - (e) => this.nativeFinder.categoryToKind(e.kind) === PythonEnvKind.OtherVirtual, - ).length; - const nativePipEnvEnvs = nativeEnvs.filter( - (e) => this.nativeFinder.categoryToKind(e.kind) === PythonEnvKind.Pipenv, - ).length; - const nativePoetryEnvs = nativeEnvs.filter( - (e) => this.nativeFinder.categoryToKind(e.kind) === PythonEnvKind.Poetry, + (e) => categoryToKind(e.kind) === PythonEnvKind.MicrosoftStore, ).length; - const nativePyenvEnvs = nativeEnvs.filter( - (e) => this.nativeFinder.categoryToKind(e.kind) === PythonEnvKind.Pyenv, - ).length; - const nativeSystemEnvs = nativeEnvs.filter( - (e) => this.nativeFinder.categoryToKind(e.kind) === PythonEnvKind.System, - ).length; - const nativeUnknownEnvs = nativeEnvs.filter( - (e) => this.nativeFinder.categoryToKind(e.kind) === PythonEnvKind.Unknown, - ).length; - const nativeVenvEnvs = nativeEnvs.filter((e) => this.nativeFinder.categoryToKind(e.kind) === PythonEnvKind.Venv) + const nativeOtherGlobalEnvs = nativeEnvs.filter((e) => categoryToKind(e.kind) === PythonEnvKind.OtherGlobal) + .length; + const nativeOtherVirtualEnvs = nativeEnvs.filter((e) => categoryToKind(e.kind) === PythonEnvKind.OtherVirtual) + .length; + const nativePipEnvEnvs = nativeEnvs.filter((e) => categoryToKind(e.kind) === PythonEnvKind.Pipenv).length; + const nativePoetryEnvs = nativeEnvs.filter((e) => categoryToKind(e.kind) === PythonEnvKind.Poetry).length; + const nativePyenvEnvs = nativeEnvs.filter((e) => categoryToKind(e.kind) === PythonEnvKind.Pyenv).length; + const nativeSystemEnvs = nativeEnvs.filter((e) => categoryToKind(e.kind) === PythonEnvKind.System).length; + const nativeUnknownEnvs = nativeEnvs.filter((e) => categoryToKind(e.kind) === PythonEnvKind.Unknown).length; + const nativeVenvEnvs = nativeEnvs.filter((e) => categoryToKind(e.kind) === PythonEnvKind.Venv).length; + const nativeVirtualEnvEnvs = nativeEnvs.filter((e) => categoryToKind(e.kind) === PythonEnvKind.VirtualEnv) .length; - const nativeVirtualEnvEnvs = nativeEnvs.filter( - (e) => this.nativeFinder.categoryToKind(e.kind) === PythonEnvKind.VirtualEnv, - ).length; const nativeVirtualEnvWrapperEnvs = nativeEnvs.filter( - (e) => this.nativeFinder.categoryToKind(e.kind) === PythonEnvKind.VirtualEnvWrapper, + (e) => categoryToKind(e.kind) === PythonEnvKind.VirtualEnvWrapper, ).length; const nativeGlobal = nativeEnvs.filter( (e) => - this.nativeFinder.categoryToKind(e.kind) === PythonEnvKind.OtherGlobal || - this.nativeFinder.categoryToKind(e.kind) === PythonEnvKind.System || - this.nativeFinder.categoryToKind(e.kind) === PythonEnvKind.Custom || - this.nativeFinder.categoryToKind(e.kind) === PythonEnvKind.OtherVirtual, + categoryToKind(e.kind) === PythonEnvKind.OtherGlobal || + categoryToKind(e.kind) === PythonEnvKind.System || + categoryToKind(e.kind) === PythonEnvKind.Custom || + categoryToKind(e.kind) === PythonEnvKind.OtherVirtual, ).length; // Intent is to capture time taken for discovery of all envs to complete the first time. @@ -618,7 +601,7 @@ export class EnvsCollectionService extends PythonEnvsWatcher fsPath.normalize(e.prefix || '').toLowerCase() === rootPrefix.toLowerCase()); condaTelemetry.condaRootPrefixEnvsAfterFind = rootPrefixEnvs.length; condaTelemetry.condaRootPrefixFoundInInfoAfterFind = !!rootPrefixEnv; @@ -1029,7 +1012,7 @@ async function getCondaTelemetry( const defaultPrefixEnvs = await flattenIterable(nativeFinder.refresh([Uri.file(defaultPrefix)])); // Did we find an env with the same prefix? const defaultPrefixEnv = defaultPrefixEnvs - .filter(isNativeInfoEnvironment) + .filter(isNativeEnvInfo) .find((e) => fsPath.normalize(e.prefix || '').toLowerCase() === defaultPrefix.toLowerCase()); condaTelemetry.condaDefaultPrefixEnvsAfterFind = defaultPrefixEnvs.length; condaTelemetry.condaDefaultPrefixFoundInInfoAfterFind = !!defaultPrefixEnv; diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/nativeLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/nativeLocator.ts deleted file mode 100644 index 6aa7be8280bc..000000000000 --- a/src/client/pythonEnvironments/base/locators/lowLevel/nativeLocator.ts +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { Disposable, Event, EventEmitter, Uri } from 'vscode'; -import { IDisposable } from '../../../../common/types'; -import { ILocator, BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; -import { PythonEnvsChangedEvent } from '../../watcher'; -import { PythonVersion } from '../../info'; -import { Conda } from '../../../common/environmentManagers/conda'; -import { traceError } from '../../../../logging'; -import type { KnownEnvironmentTools } from '../../../../api/types'; -import { setPyEnvBinary } from '../../../common/environmentManagers/pyenv'; -import { NativePythonFinder, getNativePythonFinder, isNativeInfoEnvironment } from '../common/nativePythonFinder'; -import { disposeAll } from '../../../../common/utils/resourceLifecycle'; -import { Architecture } from '../../../../common/utils/platform'; - -function toolToKnownEnvironmentTool(tool: string): KnownEnvironmentTools { - switch (tool.toLowerCase()) { - case 'conda': - return 'Conda'; - case 'poetry': - return 'Poetry'; - case 'pyenv': - return 'Pyenv'; - default: { - traceError(`Unknown Python Tool '${tool}' from Native Locator.`); - return 'Unknown'; - } - } -} - -function parseVersion(version?: string): PythonVersion | undefined { - if (!version) { - return undefined; - } - - try { - const [major, minor, micro] = version.split('.').map((v) => parseInt(v, 10)); - return { - major: typeof major === 'number' ? major : -1, - minor: typeof minor === 'number' ? minor : -1, - micro: typeof micro === 'number' ? micro : -1, - sysVersion: version, - }; - } catch { - return undefined; - } -} - -export class NativeLocator implements ILocator, IDisposable { - public readonly providerId: string = 'native-locator'; - - private readonly onChangedEmitter = new EventEmitter(); - - private readonly disposables: IDisposable[] = []; - - private readonly finder: NativePythonFinder; - - constructor() { - this.onChanged = this.onChangedEmitter.event; - this.finder = getNativePythonFinder(); - this.disposables.push(this.onChangedEmitter, this.finder); - } - - public readonly onChanged: Event; - - public async dispose(): Promise { - this.disposables.forEach((d) => d.dispose()); - return Promise.resolve(); - } - - public async *iterEnvs(): IPythonEnvsIterator { - const disposables: IDisposable[] = []; - const disposable = new Disposable(() => disposeAll(disposables)); - this.disposables.push(disposable); - for await (const data of this.finder.refresh()) { - if (isNativeInfoEnvironment(data)) { - if (data.manager) { - switch (toolToKnownEnvironmentTool(data.manager.tool)) { - case 'Conda': { - Conda.setConda(data.manager.executable); - break; - } - case 'Pyenv': { - setPyEnvBinary(data.manager.executable); - break; - } - default: { - break; - } - } - } - if (data.executable) { - const arch = (data.arch || '').toLowerCase(); - const env: BasicEnvInfo = { - kind: this.finder.categoryToKind(data.kind), - executablePath: data.executable ? data.executable : '', - envPath: data.prefix ? data.prefix : undefined, - version: data.version ? parseVersion(data.version) : undefined, - name: data.name ? data.name : '', - displayName: data.displayName ? data.displayName : '', - searchLocation: data.project ? Uri.file(data.project) : undefined, - identifiedUsingNativeLocator: true, - arch: - // eslint-disable-next-line no-nested-ternary - arch === 'x64' ? Architecture.x64 : arch === 'x86' ? Architecture.x86 : undefined, - }; - yield env; - } - } - } - } -} diff --git a/src/client/pythonEnvironments/nativeAPI.ts b/src/client/pythonEnvironments/nativeAPI.ts index 3f4455b00fd3..6690beebf7c9 100644 --- a/src/client/pythonEnvironments/nativeAPI.ts +++ b/src/client/pythonEnvironments/nativeAPI.ts @@ -13,7 +13,7 @@ import { TriggerRefreshOptions, } from './base/locator'; import { PythonEnvCollectionChangedEvent } from './base/watcher'; -import { isNativeInfoEnvironment, NativeEnvInfo, NativePythonFinder } from './base/locators/common/nativePythonFinder'; +import { isNativeEnvInfo, NativeEnvInfo, NativePythonFinder } from './base/locators/common/nativePythonFinder'; import { createDeferred, Deferred } from '../common/utils/async'; import { Architecture } from '../common/utils/platform'; import { parseVersion } from './base/info/pythonVersion'; @@ -21,6 +21,7 @@ import { cache } from '../common/utils/decorators'; import { traceError, traceLog } from '../logging'; import { StopWatch } from '../common/utils/stopWatch'; import { FileChangeType } from '../common/platform/fileSystemWatcher'; +import { categoryToKind } from './base/locators/common/nativePythonUtils'; function makeExecutablePath(prefix?: string): string { if (!prefix) { @@ -149,11 +150,11 @@ function getName(nativeEnv: NativeEnvInfo, kind: PythonEnvKind): string { return ''; } -function toPythonEnvInfo(finder: NativePythonFinder, nativeEnv: NativeEnvInfo): PythonEnvInfo | undefined { +function toPythonEnvInfo(nativeEnv: NativeEnvInfo): PythonEnvInfo | undefined { if (!validEnv(nativeEnv)) { return undefined; } - const kind = finder.categoryToKind(nativeEnv.kind); + const kind = categoryToKind(nativeEnv.kind); const arch = toArch(nativeEnv.arch); const version: PythonVersion = parseVersion(nativeEnv.version ?? ''); const name = getName(nativeEnv, kind); @@ -229,7 +230,7 @@ class NativePythonEnvironments implements IDiscoveryAPI, Disposable { setImmediate(async () => { try { for await (const native of this.finder.refresh()) { - if (!isNativeInfoEnvironment(native) || !validEnv(native)) { + if (!isNativeEnvInfo(native) || !validEnv(native)) { // eslint-disable-next-line no-continue continue; } @@ -237,7 +238,7 @@ class NativePythonEnvironments implements IDiscoveryAPI, Disposable { const envPath = native.executable ?? native.prefix; const version = native.version ? parseVersion(native.version) : undefined; - if (this.finder.categoryToKind(native.kind) === PythonEnvKind.Conda && !native.executable) { + if (categoryToKind(native.kind) === PythonEnvKind.Conda && !native.executable) { // This is a conda env without python, no point trying to resolve this. // There is nothing to resolve this.addEnv(native); @@ -288,7 +289,7 @@ class NativePythonEnvironments implements IDiscoveryAPI, Disposable { } addEnv(native: NativeEnvInfo): void { - const info = toPythonEnvInfo(this.finder, native); + const info = toPythonEnvInfo(native); if (!info) { return; } @@ -310,7 +311,7 @@ class NativePythonEnvironments implements IDiscoveryAPI, Disposable { } const native = await this.finder.resolve(envPath); if (native) { - const env = toPythonEnvInfo(this.finder, native); + const env = toPythonEnvInfo(native); if (env) { const old = this._envs.find((item) => item.executable.filename === env.executable.filename); if (old) { 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 45ce63cfbdfc..b807e337a4da 100644 --- a/src/test/pythonEnvironments/base/locators/composite/envsCollectionService.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/composite/envsCollectionService.unit.test.ts @@ -37,10 +37,6 @@ class MockNativePythonFinder implements nativeFinder.NativePythonFinder { throw new Error('Method not implemented.'); } - categoryToKind(_category: nativeFinder.PythonEnvironmentKind): PythonEnvKind { - throw new Error('Method not implemented.'); - } - resolve(_executable: string): Promise { throw new Error('Method not implemented.'); } diff --git a/src/test/pythonEnvironments/nativeAPI.unit.test.ts b/src/test/pythonEnvironments/nativeAPI.unit.test.ts index 93fc69951287..89be5dc374e2 100644 --- a/src/test/pythonEnvironments/nativeAPI.unit.test.ts +++ b/src/test/pythonEnvironments/nativeAPI.unit.test.ts @@ -9,14 +9,13 @@ import * as sinon from 'sinon'; import * as nativeAPI from '../../client/pythonEnvironments/nativeAPI'; import { IDiscoveryAPI } from '../../client/pythonEnvironments/base/locator'; import { - categoryToKind, NativeEnvInfo, NativePythonFinder, - PythonEnvironmentKind, } from '../../client/pythonEnvironments/base/locators/common/nativePythonFinder'; import { Architecture } from '../../client/common/utils/platform'; import { PythonEnvInfo, PythonEnvKind, PythonEnvType } from '../../client/pythonEnvironments/base/info'; import { isWindows } from '../../client/common/platform/platformService'; +import { NativePythonEnvironmentKind } from '../../client/pythonEnvironments/base/locators/common/nativePythonUtils'; suite('Native Python API', () => { let api: IDiscoveryAPI; @@ -26,7 +25,7 @@ suite('Native Python API', () => { displayName: 'Basic Python', name: 'basic_python', executable: '/usr/bin/python', - kind: PythonEnvironmentKind.LinuxGlobal, + kind: NativePythonEnvironmentKind.LinuxGlobal, version: `3.12.0`, prefix: '/usr/bin', }; @@ -35,7 +34,7 @@ suite('Native Python API', () => { displayName: 'Basic Python', name: 'basic_python', executable: '/usr/bin/python', - kind: PythonEnvironmentKind.LinuxGlobal, + kind: NativePythonEnvironmentKind.LinuxGlobal, version: undefined, // this is intentionally set to trigger resolve prefix: '/usr/bin', }; @@ -58,7 +57,7 @@ suite('Native Python API', () => { displayName: 'Conda Python', name: 'conda_python', executable: '/home/user/.conda/envs/conda_python/python', - kind: PythonEnvironmentKind.Conda, + kind: NativePythonEnvironmentKind.Conda, version: `3.12.0`, prefix: '/home/user/.conda/envs/conda_python', }; @@ -67,7 +66,7 @@ suite('Native Python API', () => { displayName: 'Conda Python', name: 'conda_python', executable: '/home/user/.conda/envs/conda_python/python', - kind: PythonEnvironmentKind.Conda, + kind: NativePythonEnvironmentKind.Conda, version: undefined, // this is intentionally set to test conda without python prefix: '/home/user/.conda/envs/conda_python', }; @@ -76,7 +75,7 @@ suite('Native Python API', () => { displayName: 'Conda Python', name: 'conda_python', executable: undefined, // this is intentionally set to test env with no executable - kind: PythonEnvironmentKind.Conda, + kind: NativePythonEnvironmentKind.Conda, version: undefined, // this is intentionally set to test conda without python prefix: '/home/user/.conda/envs/conda_python', }; @@ -125,11 +124,6 @@ suite('Native Python API', () => { setup(() => { mockFinder = typemoq.Mock.ofType(); - - mockFinder - .setup((f) => f.categoryToKind(typemoq.It.isAny())) - .returns((category: PythonEnvironmentKind) => categoryToKind(category)); - api = nativeAPI.createNativeEnvironmentsApi(mockFinder.object); }); diff --git a/src/test/pythonEnvironments/nativePythonFinder.unit.test.ts b/src/test/pythonEnvironments/nativePythonFinder.unit.test.ts new file mode 100644 index 000000000000..b6182da8111f --- /dev/null +++ b/src/test/pythonEnvironments/nativePythonFinder.unit.test.ts @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { WorkspaceConfiguration } from 'vscode'; +import { + getNativePythonFinder, + isNativeEnvInfo, + NativeEnvInfo, + NativePythonFinder, +} from '../../client/pythonEnvironments/base/locators/common/nativePythonFinder'; +import * as windowsApis from '../../client/common/vscodeApis/windowApis'; +import { MockOutputChannel } from '../mockClasses'; +import * as workspaceApis from '../../client/common/vscodeApis/workspaceApis'; + +suite('Native Python Finder', () => { + let finder: NativePythonFinder; + let createLogOutputChannelStub: sinon.SinonStub; + let getConfigurationStub: sinon.SinonStub; + let configMock: typemoq.IMock; + let getWorkspaceFolderPathsStub: sinon.SinonStub; + + setup(() => { + createLogOutputChannelStub = sinon.stub(windowsApis, 'createLogOutputChannel'); + createLogOutputChannelStub.returns(new MockOutputChannel('locator')); + + getWorkspaceFolderPathsStub = sinon.stub(workspaceApis, 'getWorkspaceFolderPaths'); + getWorkspaceFolderPathsStub.returns([]); + + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); + configMock = typemoq.Mock.ofType(); + configMock.setup((c) => c.get('venvPath')).returns(() => undefined); + configMock.setup((c) => c.get('venvFolders')).returns(() => []); + configMock.setup((c) => c.get('condaPath')).returns(() => ''); + configMock.setup((c) => c.get('poetryPath')).returns(() => ''); + getConfigurationStub.returns(configMock.object); + + finder = getNativePythonFinder(); + }); + + teardown(() => { + sinon.restore(); + }); + + suiteTeardown(() => { + finder.dispose(); + }); + + test('Refresh should return python environments', async () => { + const envs = []; + for await (const env of finder.refresh()) { + envs.push(env); + } + + // typically all test envs should have at least one environment + assert.isNotEmpty(envs); + }); + + test('Resolve should return python environments with version', async () => { + const envs = []; + for await (const env of finder.refresh()) { + envs.push(env); + } + + // typically all test envs should have at least one environment + assert.isNotEmpty(envs); + + // pick and env without version + const env: NativeEnvInfo | undefined = envs + .filter((e) => isNativeEnvInfo(e)) + .find((e) => e.version && e.version.length > 0 && (e.executable || (e as NativeEnvInfo).prefix)); + + if (env) { + env.version = undefined; + } else { + assert.fail('Expected at least one env with valid version'); + } + + const envPath = env.executable ?? env.prefix; + if (envPath) { + const resolved = await finder.resolve(envPath); + assert.isString(resolved.version, 'Version must be a string'); + assert.isTrue((resolved?.version?.length ?? 0) > 0, 'Version must not be empty'); + } else { + assert.fail('Expected either executable or prefix to be defined'); + } + }); +});