From 112683480d62e8fe6e5ab451ba1b8701be8ad7c7 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 22 Sep 2020 10:55:01 -0600 Subject: [PATCH 01/25] Drop `PythonEnvInfo.id`. --- src/client/pythonEnvironments/base/info/index.ts | 11 +++++------ src/client/pythonEnvironments/legacyIOC.ts | 1 - src/test/pythonEnvironments/base/common.ts | 9 +-------- 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/src/client/pythonEnvironments/base/info/index.ts b/src/client/pythonEnvironments/base/info/index.ts index 5298a3d73f9e..ced0f31390e5 100644 --- a/src/client/pythonEnvironments/base/info/index.ts +++ b/src/client/pythonEnvironments/base/info/index.ts @@ -28,6 +28,11 @@ export enum PythonEnvKind { OtherVirtual = 'virt-other' } +/** + * A (system-global) unique ID for a single Python environment. + */ +export type PythonEnvID = string; + /** * Information about a file. */ @@ -44,11 +49,6 @@ export type PythonExecutableInfo = FileInfo & { sysPrefix: string; }; -/** - * A (system-global) unique ID for a single Python environment. - */ -export type PythonEnvID = string; - /** * The most fundamental information about a Python environment. * @@ -63,7 +63,6 @@ export type PythonEnvID = string; * @prop location - the env's location (on disk), if relevant */ export type PythonEnvBaseInfo = { - id: PythonEnvID; kind: PythonEnvKind; executable: PythonExecutableInfo; // One of (name, location) must be non-empty. diff --git a/src/client/pythonEnvironments/legacyIOC.ts b/src/client/pythonEnvironments/legacyIOC.ts index fbeb6cc097a3..6079bb265441 100644 --- a/src/client/pythonEnvironments/legacyIOC.ts +++ b/src/client/pythonEnvironments/legacyIOC.ts @@ -126,7 +126,6 @@ function convertEnvInfo(info: PythonEnvInfo): PythonEnvironment { function buildEmptyEnvInfo(): PythonEnvInfo { return { - id: '', kind: PythonEnvKind.Unknown, executable: { filename: '', diff --git a/src/test/pythonEnvironments/base/common.ts b/src/test/pythonEnvironments/base/common.ts index d1d3b21cc7cc..4bddce47fddf 100644 --- a/src/test/pythonEnvironments/base/common.ts +++ b/src/test/pythonEnvironments/base/common.ts @@ -21,7 +21,6 @@ export function createEnv( versionStr: string, kind?: PythonEnvKind, executable?: string, - idStr?: string ): PythonEnvInfo { if (kind === undefined) { kind = PythonEnvKind.Unknown; @@ -29,10 +28,8 @@ export function createEnv( if (executable === undefined || executable === '') { executable = 'python'; } - const id = idStr ? idStr : `${kind}-${name}`; const version = parseVersion(versionStr); return { - id, kind, version, name, @@ -53,12 +50,8 @@ export function createLocatedEnv( versionStr: string, kind = PythonEnvKind.Unknown, executable = 'python', - idStr?: string ): PythonEnvInfo { - if (!idStr) { - idStr = `${kind}-${location}`; - } - const env = createEnv('', versionStr, kind, executable, idStr); + const env = createEnv('', versionStr, kind, executable); env.location = location; return env; } From 0d22681d8e57ab2f0364e7b2ec3f9b4f730fa34b Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 22 Sep 2020 19:10:37 -0600 Subject: [PATCH 02/25] Make buildEmptyEnvInfo() a public helper. --- .../pythonEnvironments/base/info/env.ts | 42 +++++++++++++++++-- src/client/pythonEnvironments/legacyIOC.ts | 29 +------------ 2 files changed, 39 insertions(+), 32 deletions(-) diff --git a/src/client/pythonEnvironments/base/info/env.ts b/src/client/pythonEnvironments/base/info/env.ts index 5d88a27c0710..868ce3aad840 100644 --- a/src/client/pythonEnvironments/base/info/env.ts +++ b/src/client/pythonEnvironments/base/info/env.ts @@ -3,14 +3,48 @@ import { cloneDeep } from 'lodash'; import * as path from 'path'; +import { Architecture } from '../../../common/utils/platform'; +import { arePathsSame } from '../../common/externalDependencies'; +import { areEqualVersions, areEquivalentVersions } from './pythonVersion'; + import { FileInfo, PythonDistroInfo, - PythonEnvInfo, PythonEnvKind, PythonVersion, + PythonEnvInfo, + PythonEnvKind, + PythonReleaseLevel, + PythonVersion, } from '.'; -import { Architecture } from '../../../common/utils/platform'; -import { arePathsSame } from '../../common/externalDependencies'; -import { areEqualVersions, areEquivalentVersions } from './pythonVersion'; + +/** + * Create a new info object with all values empty. + */ +export function buildEmptyEnvInfo(): PythonEnvInfo { + return { + kind: PythonEnvKind.Unknown, + executable: { + filename: '', + sysPrefix: '', + ctime: -1, + mtime: -1, + }, + name: '', + location: '', + version: { + major: -1, + minor: -1, + micro: -1, + release: { + level: PythonReleaseLevel.Final, + serial: 0, + }, + }, + arch: Architecture.Unknown, + distro: { + org: '', + }, + }; +} /** * Checks if two environments are same. diff --git a/src/client/pythonEnvironments/legacyIOC.ts b/src/client/pythonEnvironments/legacyIOC.ts index 6079bb265441..f999db8b63fa 100644 --- a/src/client/pythonEnvironments/legacyIOC.ts +++ b/src/client/pythonEnvironments/legacyIOC.ts @@ -4,7 +4,6 @@ import { injectable } from 'inversify'; import * as vscode from 'vscode'; import { createDeferred } from '../common/utils/async'; -import { Architecture } from '../common/utils/platform'; import { getVersionString, parseVersion } from '../common/utils/version'; import { CONDA_ENV_FILE_SERVICE, @@ -29,6 +28,7 @@ import { import { IPipEnvServiceHelper, IPythonInPathCommandProvider } from '../interpreter/locators/types'; import { IServiceContainer, IServiceManager } from '../ioc/types'; import { PythonEnvInfo, PythonEnvKind, PythonReleaseLevel } from './base/info'; +import { buildEmptyEnvInfo } from './base/info/env'; import { ILocator, PythonLocatorQuery } from './base/locator'; import { initializeExternalDependencies } from './common/externalDependencies'; import { PythonInterpreterLocatorService } from './discovery/locators'; @@ -124,33 +124,6 @@ function convertEnvInfo(info: PythonEnvInfo): PythonEnvironment { return env; } -function buildEmptyEnvInfo(): PythonEnvInfo { - return { - kind: PythonEnvKind.Unknown, - executable: { - filename: '', - sysPrefix: '', - ctime: -1, - mtime: -1, - }, - name: '', - location: '', - version: { - major: -1, - minor: -1, - micro: -1, - release: { - level: PythonReleaseLevel.Final, - serial: 0, - }, - }, - arch: Architecture.Unknown, - distro: { - org: '', - }, - }; -} - interface IPythonEnvironments extends ILocator {} @injectable() From 4468e745c2eb8a380f3805a7aa22a9a19d595889 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 22 Sep 2020 19:11:26 -0600 Subject: [PATCH 03/25] Add a copyEnvInfo() helper. --- .../pythonEnvironments/base/info/env.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/client/pythonEnvironments/base/info/env.ts b/src/client/pythonEnvironments/base/info/env.ts index 868ce3aad840..dc543743bdf7 100644 --- a/src/client/pythonEnvironments/base/info/env.ts +++ b/src/client/pythonEnvironments/base/info/env.ts @@ -46,6 +46,36 @@ export function buildEmptyEnvInfo(): PythonEnvInfo { }; } +/** + * Return a deep copy of the given env info. + * + * @param updates - if provided, these values are applied to the copy + */ +export function copyEnvInfo( + env: PythonEnvInfo, + updates?: { + kind?: PythonEnvKind, + }, +): PythonEnvInfo { + const copied = { ...env }; + copied.version = { ...env.version }; + copied.version.release = { ...env.version.release }; + copied.executable = { ...env.executable }; + copied.distro = { ...env.distro }; + if (copied.distro.version !== undefined) { + copied.distro.version = { ...env.distro.version! }; + } + // We don't bother with env.searchLocation. + + // Apply updates. + if (updates !== undefined) { + if (updates.kind !== undefined) { + copied.kind = updates.kind; + } + } + return copied; +} + /** * Checks if two environments are same. * @param {string | PythonEnvInfo} left: environment to compare. From 94137c8fb8a9457dbd178fa38f09e4f86f9553c3 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Mon, 21 Sep 2020 17:13:41 -0600 Subject: [PATCH 04/25] Add locator utils. --- .../pythonEnvironments/base/locatorUtils.ts | 77 ++++ src/test/pythonEnvironments/base/common.ts | 45 ++- .../base/locatorUtils.unit.test.ts | 381 ++++++++++++++++++ 3 files changed, 497 insertions(+), 6 deletions(-) create mode 100644 src/client/pythonEnvironments/base/locatorUtils.ts create mode 100644 src/test/pythonEnvironments/base/locatorUtils.unit.test.ts diff --git a/src/client/pythonEnvironments/base/locatorUtils.ts b/src/client/pythonEnvironments/base/locatorUtils.ts new file mode 100644 index 000000000000..b7b67a8ce5c2 --- /dev/null +++ b/src/client/pythonEnvironments/base/locatorUtils.ts @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { createDeferred } from '../../common/utils/async'; +import { PythonEnvInfo } from './info'; +import { isSameEnv } from './info/env'; +import { + IPythonEnvsIterator, + PythonEnvUpdatedEvent, + PythonLocatorQuery, +} from './locator'; + +/** + * Create a filter function to match the given query. + */ +export function getQueryFilter(query: PythonLocatorQuery): (env: PythonEnvInfo) => boolean { + const locations = (query.searchLocations !== undefined && query.searchLocations.length > 0) + ? query.searchLocations + .filter((loc) => loc !== undefined) + .map((loc) => loc.toString()) + : undefined; + + return (env) => { + if (query.kinds !== undefined && query.kinds.length > 0) { + if (!query.kinds.includes(env.kind)) { + return false; + } + } + if (locations !== undefined) { + if (env.searchLocation === undefined) { + return false; + } + const loc = env.searchLocation.toString(); + if (!locations.some((l) => l === loc)) { + return false; + } + } + return true; + }; +} + +/** + * Unroll the given iterator into an array. + * + * This includes applying any received updates. + */ +export async function getEnvs(iterator: IPythonEnvsIterator): Promise { + const envs: PythonEnvInfo[] = []; + + const updatesDone = createDeferred(); + if (iterator.onUpdated === undefined) { + updatesDone.resolve(); + } else { + iterator.onUpdated((event: PythonEnvUpdatedEvent | null) => { + if (event === null) { + updatesDone.resolve(); + return; + } + const oldEnv = envs.find((e) => isSameEnv(e, event.old)); + if (oldEnv === undefined) { + // XXX log or fail + } else { + envs[envs.indexOf(oldEnv)] = event.new; + } + }); + } + + let result = await iterator.next(); + while (!result.done) { + envs.push(result.value); + // eslint-disable-next-line no-await-in-loop + result = await iterator.next(); + } + + await updatesDone.promise; + return envs; +} diff --git a/src/test/pythonEnvironments/base/common.ts b/src/test/pythonEnvironments/base/common.ts index 4bddce47fddf..9d44b161ef0b 100644 --- a/src/test/pythonEnvironments/base/common.ts +++ b/src/test/pythonEnvironments/base/common.ts @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import * as path from 'path'; import { Event } from 'vscode'; import { createDeferred, flattenIterator, iterable, mapToIterator, @@ -16,18 +17,20 @@ import { } from '../../../client/pythonEnvironments/base/locator'; import { PythonEnvsChangedEvent } from '../../../client/pythonEnvironments/base/watcher'; +export function fixPath(filename: string): string { + return path.normalize(filename); +} + export function createEnv( name: string, versionStr: string, kind?: PythonEnvKind, - executable?: string, + execStr = 'python', ): PythonEnvInfo { if (kind === undefined) { kind = PythonEnvKind.Unknown; } - if (executable === undefined || executable === '') { - executable = 'python'; - } + const executable = execStr === '' ? 'python' : fixPath(execStr); const version = parseVersion(versionStr); return { kind, @@ -46,16 +49,46 @@ export function createEnv( } export function createLocatedEnv( - location: string, + locationStr: string, versionStr: string, kind = PythonEnvKind.Unknown, - executable = 'python', + execStr = 'python', ): PythonEnvInfo { + const location = fixPath(locationStr); + const normalizedExecutable = fixPath(execStr); + const executable = path.isAbsolute(normalizedExecutable) + ? normalizedExecutable + : path.join(location, 'bin', normalizedExecutable); const env = createEnv('', versionStr, kind, executable); env.location = location; return env; } +export function copyEnv( + env: PythonEnvInfo, + updates?: { + kind?: PythonEnvKind, + }, +): PythonEnvInfo { + const copied = { ...env }; + copied.version = { ...env.version }; + copied.version.release = { ...env.version.release }; + copied.executable = { ...env.executable }; + copied.distro = { ...env.distro }; + if (copied.distro.version !== undefined) { + copied.distro.version = { ...env.distro.version! }; + } + // We don't bother with env.searchLocation. + + // Apply updates. + if (updates !== undefined) { + if (updates.kind !== undefined) { + copied.kind = updates.kind; + } + } + return copied; +} + export class SimpleLocator extends Locator { private deferred = createDeferred(); constructor( diff --git a/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts b/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts new file mode 100644 index 000000000000..2fc5a3973a1a --- /dev/null +++ b/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts @@ -0,0 +1,381 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as path from 'path'; +import { EventEmitter, Uri } from 'vscode'; +import { getValues as getEnumValues } from '../../../client/common/utils/enum'; +import { PythonEnvInfo, PythonEnvKind } from '../../../client/pythonEnvironments/base/info'; +import { copyEnvInfo } from '../../../client/pythonEnvironments/base/info/env'; +import { + IPythonEnvsIterator, + PythonEnvUpdatedEvent, + PythonLocatorQuery, +} from '../../../client/pythonEnvironments/base/locator'; +import { getEnvs, getQueryFilter } from '../../../client/pythonEnvironments/base/locatorUtils'; +import { + createEnv, + createLocatedEnv, + fixPath, +} from './common'; + +const homeDir = fixPath('/home/me'); +const workspaceRoot = Uri.file('workspace-root'); +const doesNotExist = Uri.file(fixPath('does-not-exist')); + +function setSearchLocation(env: PythonEnvInfo, location?: string): void { + const locationStr = location === undefined + ? path.dirname(env.location) + : fixPath(location); + env.searchLocation = Uri.file(locationStr); +} + +const env1 = createEnv('env1', '3.8', PythonEnvKind.System, '/usr/bin/python3.8'); +const env2 = createEnv('env2', '3.8.1rc2', PythonEnvKind.Pyenv, '/pyenv/3.8.1rc2/bin/python'); +const env3 = createEnv('env3', '3.9.1b2', PythonEnvKind.Unknown, 'python3.9'); +const env4 = createEnv('env4', '2.7.11', PythonEnvKind.Pyenv, '/pyenv/2.7.11/bin/python'); +const env5 = createEnv('env5', '2.7', PythonEnvKind.System, 'python2'); +const env6 = createEnv('env6', '3.7.4', PythonEnvKind.Conda, 'python'); +const plainEnvs = [env1, env2, env3, env4, env5, env6]; + +const envL1 = createLocatedEnv('/.venvs/envL1', '3.9.0', PythonEnvKind.Venv); +const envL2 = createLocatedEnv('/conda/envs/envL2', '3.8.3', PythonEnvKind.Conda); +const locatedEnvs = [envL1, envL2]; + +const envS1 = createEnv('env S1', '3.9', PythonEnvKind.OtherVirtual, `${homeDir}/some-dir/bin/python`); +setSearchLocation(envS1, homeDir); +const envS2 = createEnv('env S2', '3.9', PythonEnvKind.OtherVirtual, `${homeDir}/some-dir2/bin/python`); +setSearchLocation(envS2, homeDir); +const rootedEnvs = [envS1, envS2]; + +const envSL1 = createLocatedEnv(`${homeDir}/.venvs/envSL1`, '3.9.0', PythonEnvKind.Venv); +setSearchLocation(envSL1); +const envSL2 = createLocatedEnv(`${workspaceRoot.fsPath}/.venv`, '3.8.2', PythonEnvKind.Pipenv); +setSearchLocation(envSL2); +const envSL3 = createLocatedEnv(`${homeDir}/.conda-envs/envSL3`, '3.8.2', PythonEnvKind.Conda); +setSearchLocation(envSL3); +const envSL4 = createLocatedEnv('/opt/python3.10', '3.10.0a1', PythonEnvKind.Custom); +setSearchLocation(envSL4); +const envSL5 = createLocatedEnv(`${homeDir}/.venvs/envSL5`, '3.9.0', PythonEnvKind.Venv); +setSearchLocation(envSL5); +const rootedLocatedEnvs = [envSL1, envSL2, envSL3, envSL4, envSL5]; + +const envs = [ + ...plainEnvs, + ...locatedEnvs, + ...rootedEnvs, + ...rootedLocatedEnvs, +]; + +suite('Python envs locator utils - getQueryFilter', () => { + suite('empty query', () => { + const queries: PythonLocatorQuery[] = [ + {}, + { kinds: [] }, + { searchLocations: [] }, + { kinds: [], searchLocations: [] }, + ]; + queries.forEach((query) => { + test(`all envs kept (query ${query})`, () => { + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, envs); + }); + }); + }); + + suite('kinds', () => { + test('match none', () => { + const query: PythonLocatorQuery = { kinds: [PythonEnvKind.MacDefault] }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, []); + }); + + ([ + [PythonEnvKind.Unknown, [env3]], + [PythonEnvKind.System, [env1, env5]], + [PythonEnvKind.WindowsStore, []], + [PythonEnvKind.Pyenv, [env2, env4]], + [PythonEnvKind.Venv, [envL1, envSL1, envSL5]], + [PythonEnvKind.Conda, [env6, envL2, envSL3]], + ] as [PythonEnvKind, PythonEnvInfo[]][]).forEach(([kind, expected]) => { + test(`match some (one kind: ${kind})`, () => { + const query: PythonLocatorQuery = { kinds: [kind] }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + }); + + test('match some (many kinds)', () => { + const expected = [env6, envL1, envL2, envSL1, envSL2, envSL3, envSL4, envSL5]; + const kinds = [ + PythonEnvKind.Venv, + PythonEnvKind.VirtualEnv, + PythonEnvKind.Pipenv, + PythonEnvKind.Conda, + PythonEnvKind.Custom, + ]; + const query: PythonLocatorQuery = { kinds }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match all', () => { + const kinds: PythonEnvKind[] = getEnumValues(PythonEnvKind); + const query: PythonLocatorQuery = { kinds }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, envs); + }); + }); + + suite('searchLocations', () => { + test('match none', () => { + const query: PythonLocatorQuery = { searchLocations: [doesNotExist] }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, []); + }); + + test('match one (multiple locations)', () => { + const expected = [envSL4]; + const searchLocations = [ + envSL4.searchLocation!, + doesNotExist, + envSL4.searchLocation!, // repeated + ]; + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match multiple (one location)', () => { + const expected = [envS1, envS2]; + const searchLocations = [ + envS1.searchLocation!, + ]; + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match multiple (multiple locations)', () => { + const expected = rootedLocatedEnvs; + const searchLocations = rootedLocatedEnvs.map((env) => env.searchLocation!); + searchLocations.push(doesNotExist); + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match all', () => { + const expected = [...rootedEnvs, ...rootedLocatedEnvs]; + const searchLocations = expected.map((env) => env.searchLocation!); + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + }); + + suite('mixed query', () => { + test('match none', () => { + const query: PythonLocatorQuery = { + kinds: [PythonEnvKind.OtherGlobal], + searchLocations: [doesNotExist], + }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, []); + }); + + test('match some', () => { + const expected = [envSL1, envSL4, envSL5]; + const kinds = [PythonEnvKind.Venv, PythonEnvKind.Custom]; + const searchLocations = rootedLocatedEnvs.map((env) => env.searchLocation!); + searchLocations.push(doesNotExist); + const query: PythonLocatorQuery = { kinds, searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match all', () => { + const expected = [...rootedEnvs, ...rootedLocatedEnvs]; + const kinds: PythonEnvKind[] = getEnumValues(PythonEnvKind); + const searchLocations = expected.map((env) => env.searchLocation!); + const query: PythonLocatorQuery = { kinds, searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + }); +}); + +suite('Python envs locator utils - getEnvs', () => { + test('empty, no update emitter', async () => { + const iterator = (async function* () { + // Yield nothing. + }()) as IPythonEnvsIterator; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, []); + }); + + test('empty, with unused update emitter', async () => { + const emitter = new EventEmitter(); + // eslint-disable-next-line require-yield + const iterator = (async function* () { + // Yield nothing. + emitter.fire(null); + }()) as IPythonEnvsIterator; + iterator.onUpdated = emitter.event; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, []); + }); + + test('yield one, no update emitter', async () => { + const iterator = (async function* () { + yield env1; + }()) as IPythonEnvsIterator; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, [env1]); + }); + + test('yield one, no update', async () => { + const emitter = new EventEmitter(); + const iterator = (async function* () { + yield env1; + emitter.fire(null); + }()) as IPythonEnvsIterator; + iterator.onUpdated = emitter.event; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, [env1]); + }); + + test('yield one, with update', async () => { + const expected = [envSL2]; + const old = copyEnvInfo(envSL2, { kind: PythonEnvKind.Venv }); + const emitter = new EventEmitter(); + const iterator = (async function* () { + yield old; + emitter.fire({ old, new: envSL2 }); + emitter.fire(null); + }()) as IPythonEnvsIterator; + iterator.onUpdated = emitter.event; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, expected); + }); + + test('yield many, no update emitter', async () => { + const expected = rootedLocatedEnvs; + const iterator = (async function* () { + yield* expected; + }()) as IPythonEnvsIterator; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, expected); + }); + + test('yield many, none updated', async () => { + const expected = rootedLocatedEnvs; + const emitter = new EventEmitter(); + const iterator = (async function* () { + yield* expected; + emitter.fire(null); + }()) as IPythonEnvsIterator; + iterator.onUpdated = emitter.event; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, expected); + }); + + test('yield many, some updated', async () => { + const expected = rootedLocatedEnvs; + const emitter = new EventEmitter(); + const iterator = (async function* () { + const kind = PythonEnvKind.Unknown; + const original = [...expected]; + original[1] = copyEnvInfo(expected[1], { kind }); + original[2] = copyEnvInfo(expected[2], { kind }); + original[4] = copyEnvInfo(expected[4], { kind }); + + yield* original; + emitter.fire({ old: original[1], new: expected[1] }); + emitter.fire({ old: original[2], new: expected[2] }); + emitter.fire({ old: original[4], new: expected[4] }); + emitter.fire(null); + }()) as IPythonEnvsIterator; + iterator.onUpdated = emitter.event; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, expected); + }); + + test('yield many, all updated', async () => { + return; + const expected = rootedLocatedEnvs; + const emitter = new EventEmitter(); + const iterator = (async function* () { + const kind = PythonEnvKind.Unknown; + const original = expected.map((env) => copyEnvInfo(env, { kind })); + + yield original[0]; + yield original[1]; + emitter.fire({ old: original[0], new: expected[0] }); + yield* original.slice(2); + original.slice(1).forEach((old, index) => { + emitter.fire({ old, new: expected[index] }); + }); + emitter.fire(null); + }()) as IPythonEnvsIterator; + iterator.onUpdated = emitter.event; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, expected); + }); +}); From eea499b53374e4c61793bcd3f0ae3dbdddf802dd Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 22 Sep 2020 18:27:01 -0600 Subject: [PATCH 05/25] Use getEnvs() in ComponentAdapter.getInterpreters(). --- src/client/pythonEnvironments/legacyIOC.ts | 36 ++-------------------- 1 file changed, 3 insertions(+), 33 deletions(-) diff --git a/src/client/pythonEnvironments/legacyIOC.ts b/src/client/pythonEnvironments/legacyIOC.ts index f999db8b63fa..5675a0c3c8c1 100644 --- a/src/client/pythonEnvironments/legacyIOC.ts +++ b/src/client/pythonEnvironments/legacyIOC.ts @@ -3,7 +3,6 @@ import { injectable } from 'inversify'; import * as vscode from 'vscode'; -import { createDeferred } from '../common/utils/async'; import { getVersionString, parseVersion } from '../common/utils/version'; import { CONDA_ENV_FILE_SERVICE, @@ -30,6 +29,7 @@ import { IServiceContainer, IServiceManager } from '../ioc/types'; import { PythonEnvInfo, PythonEnvKind, PythonReleaseLevel } from './base/info'; import { buildEmptyEnvInfo } from './base/info/env'; import { ILocator, PythonLocatorQuery } from './base/locator'; +import { getEnvs } from './base/locatorUtils'; import { initializeExternalDependencies } from './common/externalDependencies'; import { PythonInterpreterLocatorService } from './discovery/locators'; import { InterpreterLocatorHelper } from './discovery/locators/helpers'; @@ -266,39 +266,9 @@ class ComponentAdapter implements IComponentAdapter { } } - const deferred = createDeferred(); - const envs: PythonEnvironment[] = []; - const executableToLegacy: Record = {}; const iterator = this.api.iterEnvs(query); - - if (iterator.onUpdated !== undefined) { - iterator.onUpdated((event) => { - if (event === null) { - deferred.resolve(envs); - } else { - // Replace the old one. - const old = executableToLegacy[event.old.executable.filename]; - if (old !== undefined) { - const index = envs.indexOf(old); - if (index !== -1) { - envs[index] = convertEnvInfo(event.new); - } - } - } - }); - } else { - deferred.resolve(envs); - } - - let res = await iterator.next(); - while (!res.done) { - const env = convertEnvInfo(res.value); - envs.push(env); - executableToLegacy[env.path] = env; - res = await iterator.next(); // eslint-disable-line no-await-in-loop - } - - return deferred.promise; + const envs = await getEnvs(iterator); + return envs.map(convertEnvInfo); } } From 7a2fdfc5fac0b40e8617fd5236b97965decb14b6 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 22 Sep 2020 18:27:57 -0600 Subject: [PATCH 06/25] Add PythonEnvUpdatedEvent.index. --- src/client/pythonEnvironments/base/locator.ts | 11 ++++---- .../pythonEnvironments/base/locatorUtils.ts | 5 ++-- .../base/locatorUtils.unit.test.ts | 26 +++++++++++-------- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/client/pythonEnvironments/base/locator.ts b/src/client/pythonEnvironments/base/locator.ts index 03eb206445bd..050a8b0afd35 100644 --- a/src/client/pythonEnvironments/base/locator.ts +++ b/src/client/pythonEnvironments/base/locator.ts @@ -15,17 +15,18 @@ import { * A single update to a previously provided Python env object. */ export type PythonEnvUpdatedEvent = { + /** + * The iteration index of The env info that was previously provided. + */ + index: number; /** * The env info that was previously provided. - * - * If the event comes from `IPythonEnvsIterator.onUpdated` then - * `old` was previously yielded during iteration. */ - old: PythonEnvInfo; + old?: PythonEnvInfo; /** * The env info that replaces the old info. */ - new: PythonEnvInfo; + update: PythonEnvInfo; }; /** diff --git a/src/client/pythonEnvironments/base/locatorUtils.ts b/src/client/pythonEnvironments/base/locatorUtils.ts index b7b67a8ce5c2..aa086278a801 100644 --- a/src/client/pythonEnvironments/base/locatorUtils.ts +++ b/src/client/pythonEnvironments/base/locatorUtils.ts @@ -3,7 +3,6 @@ import { createDeferred } from '../../common/utils/async'; import { PythonEnvInfo } from './info'; -import { isSameEnv } from './info/env'; import { IPythonEnvsIterator, PythonEnvUpdatedEvent, @@ -56,11 +55,11 @@ export async function getEnvs(iterator: IPythonEnvsIterator): Promise isSameEnv(e, event.old)); + const oldEnv = envs[event.index]; if (oldEnv === undefined) { // XXX log or fail } else { - envs[envs.indexOf(oldEnv)] = event.new; + envs[event.index] = event.update; } }); } diff --git a/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts b/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts index 2fc5a3973a1a..41b408761c87 100644 --- a/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts +++ b/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts @@ -297,7 +297,7 @@ suite('Python envs locator utils - getEnvs', () => { const emitter = new EventEmitter(); const iterator = (async function* () { yield old; - emitter.fire({ old, new: envSL2 }); + emitter.fire({ index: 0, old, update: envSL2 }); emitter.fire(null); }()) as IPythonEnvsIterator; iterator.onUpdated = emitter.event; @@ -336,16 +336,18 @@ suite('Python envs locator utils - getEnvs', () => { const expected = rootedLocatedEnvs; const emitter = new EventEmitter(); const iterator = (async function* () { - const kind = PythonEnvKind.Unknown; const original = [...expected]; - original[1] = copyEnvInfo(expected[1], { kind }); - original[2] = copyEnvInfo(expected[2], { kind }); - original[4] = copyEnvInfo(expected[4], { kind }); + const updated = [1, 2, 4]; + const kind = PythonEnvKind.Unknown; + updated.forEach((index) => { + original[index] = copyEnvInfo(expected[index], { kind }); + }); yield* original; - emitter.fire({ old: original[1], new: expected[1] }); - emitter.fire({ old: original[2], new: expected[2] }); - emitter.fire({ old: original[4], new: expected[4] }); + + updated.forEach((index) => { + emitter.fire({ index, old: original[index], update: expected[index] }); + }); emitter.fire(null); }()) as IPythonEnvsIterator; iterator.onUpdated = emitter.event; @@ -365,10 +367,12 @@ suite('Python envs locator utils - getEnvs', () => { yield original[0]; yield original[1]; - emitter.fire({ old: original[0], new: expected[0] }); + emitter.fire({ index: 0, old: original[0], update: expected[0] }); yield* original.slice(2); - original.slice(1).forEach((old, index) => { - emitter.fire({ old, new: expected[index] }); + original.forEach((old, index) => { + if (index > 0) { + emitter.fire({ index, old, update: expected[index] }); + } }); emitter.fire(null); }()) as IPythonEnvsIterator; From ca2ec73d8649772b0537a1d124da9d4d36850053 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Mon, 28 Sep 2020 11:00:50 -0600 Subject: [PATCH 07/25] Fix reducer and resolver. --- .../pythonEnvironments/base/info/env.ts | 6 ++-- .../locators/composite/environmentsReducer.ts | 20 ++++++------ .../composite/environmentsResolver.ts | 14 ++++---- .../environmentsReducer.unit.test.ts | 32 ++++++++++++------- .../environmentsResolver.unit.test.ts | 10 +++--- 5 files changed, 46 insertions(+), 36 deletions(-) diff --git a/src/client/pythonEnvironments/base/info/env.ts b/src/client/pythonEnvironments/base/info/env.ts index dc543743bdf7..e73b5e23d154 100644 --- a/src/client/pythonEnvironments/base/info/env.ts +++ b/src/client/pythonEnvironments/base/info/env.ts @@ -88,7 +88,7 @@ export function copyEnvInfo( * to be same environment. This later case is needed for comparing windows store python, * where multiple versions of python executables are all put in the same directory. */ -export function areSameEnvironment( +export function areSameEnv( left: string | PythonEnvInfo, right: string | PythonEnvInfo, allowPartialMatch?: boolean, @@ -136,11 +136,11 @@ function getPythonVersionInfoHeuristic(version:PythonVersion): number { infoLevel += 5; // W2 } - if (version.release.level) { + if (version.release?.level) { infoLevel += 3; // W1 } - if (version.release.serial || version.sysVersion) { + if (version.release?.serial || version.sysVersion) { infoLevel += 1; // W0 } diff --git a/src/client/pythonEnvironments/base/locators/composite/environmentsReducer.ts b/src/client/pythonEnvironments/base/locators/composite/environmentsReducer.ts index 617160c05b11..6c3a71444df8 100644 --- a/src/client/pythonEnvironments/base/locators/composite/environmentsReducer.ts +++ b/src/client/pythonEnvironments/base/locators/composite/environmentsReducer.ts @@ -6,7 +6,7 @@ import { Event, EventEmitter } from 'vscode'; import { traceVerbose } from '../../../../common/logger'; import { createDeferred } from '../../../../common/utils/async'; import { PythonEnvInfo, PythonEnvKind } from '../../info'; -import { areSameEnvironment } from '../../info/env'; +import { areSameEnv } from '../../info/env'; import { ILocator, IPythonEnvsIterator, PythonEnvUpdatedEvent, PythonLocatorQuery, } from '../../locator'; @@ -29,13 +29,13 @@ export class PythonEnvsReducer implements ILocator { iterator.onUpdated!((event) => { if (event === null) { waitForUpdatesDeferred.resolve(); - } else if (environment && areSameEnvironment(environment, event.new)) { - environment = event.new; + } else if (environment && areSameEnv(environment, event.update)) { + environment = event.update; } }); let result = await iterator.next(); while (!result.done) { - if (areSameEnvironment(result.value, env)) { + if (areSameEnv(result.value, env)) { environment = result.value; } // eslint-disable-next-line no-await-in-loop @@ -73,13 +73,13 @@ async function* iterEnvsIterator( state.done = true; checkIfFinishedAndNotify(state, didUpdate); } else { - const oldIndex = seen.findIndex((s) => areSameEnvironment(s, event.old)); - if (oldIndex !== -1) { + if (seen[event.index] !== undefined) { state.pending += 1; - resolveDifferencesInBackground(oldIndex, event.new, state, didUpdate, seen).ignoreErrors(); + resolveDifferencesInBackground(event.index, event.update, state, didUpdate, seen) + .ignoreErrors(); } else { // This implies a problem in a downstream locator - traceVerbose(`Expected already iterated env, got ${event.old}`); + traceVerbose(`Expected already iterated env, got ${event.old} (#${event.index})`); } } }); @@ -88,7 +88,7 @@ async function* iterEnvsIterator( let result = await iterator.next(); while (!result.done) { const currEnv = result.value; - const oldIndex = seen.findIndex((s) => areSameEnvironment(s, currEnv)); + const oldIndex = seen.findIndex((s) => areSameEnv(s, currEnv)); if (oldIndex !== -1) { state.pending += 1; resolveDifferencesInBackground(oldIndex, currEnv, state, didUpdate, seen).ignoreErrors(); @@ -116,8 +116,8 @@ async function resolveDifferencesInBackground( const oldEnv = seen[oldIndex]; const merged = mergeEnvironments(oldEnv, newEnv); if (!isEqual(oldEnv, merged)) { - didUpdate.fire({ old: oldEnv, new: merged }); seen[oldIndex] = merged; + didUpdate.fire({ index: oldIndex, old: oldEnv, update: merged }); } state.pending -= 1; checkIfFinishedAndNotify(state, didUpdate); diff --git a/src/client/pythonEnvironments/base/locators/composite/environmentsResolver.ts b/src/client/pythonEnvironments/base/locators/composite/environmentsResolver.ts index 83ea21e726e5..305c9f22824f 100644 --- a/src/client/pythonEnvironments/base/locators/composite/environmentsResolver.ts +++ b/src/client/pythonEnvironments/base/locators/composite/environmentsResolver.ts @@ -6,7 +6,6 @@ import { Event, EventEmitter } from 'vscode'; import { traceVerbose } from '../../../../common/logger'; import { IEnvironmentInfoService } from '../../../info/environmentInfoService'; import { PythonEnvInfo } from '../../info'; -import { areSameEnvironment } from '../../info/env'; import { InterpreterInformation } from '../../info/interpreter'; import { ILocator, IPythonEnvsIterator, PythonEnvUpdatedEvent, PythonLocatorQuery, @@ -63,14 +62,14 @@ export class PythonEnvsResolver implements ILocator { state.done = true; checkIfFinishedAndNotify(state, didUpdate); } else { - const oldIndex = seen.findIndex((s) => areSameEnvironment(s, event.old)); - if (oldIndex !== -1) { - seen[oldIndex] = event.new; + if (seen[event.index] !== undefined) { + seen[event.index] = event.update; state.pending += 1; - this.resolveInBackground(oldIndex, state, didUpdate, seen).ignoreErrors(); + this.resolveInBackground(event.index, state, didUpdate, seen) + .ignoreErrors(); } else { // This implies a problem in a downstream locator - traceVerbose(`Expected already iterated env in resolver, got ${event.old}`); + traceVerbose(`Expected already iterated env, got ${event.old} (#${event.index})`); } } }); @@ -103,8 +102,9 @@ export class PythonEnvsResolver implements ILocator { ); if (interpreterInfo) { const resolvedEnv = getResolvedEnv(interpreterInfo, seen[envIndex]); - didUpdate.fire({ old: seen[envIndex], new: resolvedEnv }); + const old = seen[envIndex]; seen[envIndex] = resolvedEnv; + didUpdate.fire({ old, index: envIndex, update: resolvedEnv }); } state.pending -= 1; checkIfFinishedAndNotify(state, didUpdate); diff --git a/src/test/pythonEnvironments/base/locators/composite/environmentsReducer.unit.test.ts b/src/test/pythonEnvironments/base/locators/composite/environmentsReducer.unit.test.ts index 9bb67f672768..4977bdb53699 100644 --- a/src/test/pythonEnvironments/base/locators/composite/environmentsReducer.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/composite/environmentsReducer.unit.test.ts @@ -15,7 +15,7 @@ import { PythonEnvsChangedEvent } from '../../../../../client/pythonEnvironments import { sleep } from '../../../../core'; import { createEnv, getEnvs, SimpleLocator } from '../../common'; -suite('Environments Reducer', () => { +suite('Python envs locator - Environments Reducer', () => { suite('iterEnvs()', () => { test('Iterator only yields unique environments', async () => { const env1 = createEnv('env1', '3.5', PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); @@ -64,8 +64,8 @@ suite('Environments Reducer', () => { // Assert const expectedUpdates = [ - { old: env2, new: mergeEnvironments(env2, env4) }, - { old: env1, new: mergeEnvironments(env1, env5) }, + { index: 1, old: env2, update: mergeEnvironments(env2, env4) }, + { index: 0, old: env1, update: mergeEnvironments(env1, env5) }, null, ]; assert.deepEqual(expectedUpdates, onUpdatedEvents); @@ -102,9 +102,16 @@ suite('Environments Reducer', () => { const env123 = mergeEnvironments(env12, env3); const expectedUpdates: (PythonEnvUpdatedEvent | null)[] = []; if (isEqual(env12, env123)) { - expectedUpdates.push({ old: env1, new: env12 }, null); + expectedUpdates.push( + { index: 0, old: env1, update: env12 }, + null, + ); } else { - expectedUpdates.push({ old: env1, new: env12 }, { old: env12, new: env123 }, null); + expectedUpdates.push( + { index: 0, old: env1, update: env12 }, + { index: 0, old: env12, update: env123 }, + null, + ); } assert.deepEqual(onUpdatedEvents, expectedUpdates); }); @@ -133,12 +140,15 @@ suite('Environments Reducer', () => { // Act await getEnvs(iterator); - didUpdate.fire({ old: env1, new: env2 }); + didUpdate.fire({ index: 0, old: env1, update: env2 }); didUpdate.fire(null); // It is essential for the incoming iterator to fire "null" event signifying it's done await sleep(1); // Assert - const expectedUpdates = [{ old: env1, new: mergeEnvironments(env1, env2) }, null]; + const expectedUpdates = [ + { index: 0, old: env1, update: mergeEnvironments(env1, env2) }, + null, + ]; assert.deepEqual(expectedUpdates, onUpdatedEvents); didUpdate.dispose(); }); @@ -172,11 +182,11 @@ suite('Environments Reducer', () => { const env13 = mergeEnvironments(env1, env3); const env136 = mergeEnvironments(env13, env6); - const expectedResolvedEnv = createEnv('resolvedEnv', '3.8.1', PythonEnvKind.Conda, 'resolved/path/to/exec'); + const expected = createEnv('resolvedEnv', '3.8.1', PythonEnvKind.Conda, 'resolved/path/to/exec'); const parentLocator = new SimpleLocator(environmentsToBeIterated, { resolve: async (e: PythonEnvInfo) => { if (isEqual(e, env136)) { - return expectedResolvedEnv; + return expected; } return undefined; }, @@ -184,9 +194,9 @@ suite('Environments Reducer', () => { const reducer = new PythonEnvsReducer(parentLocator); // Trying to resolve the environment corresponding to env1 env3 env6 - const expected = await reducer.resolveEnv(path.join('path', 'to', 'exec')); + const resolved = await reducer.resolveEnv(path.join('path', 'to', 'exec')); - assert.deepEqual(expected, expectedResolvedEnv); + assert.deepEqual(resolved, expected); }); test("If the reducer isn't able to resolve environment, return undefined", async () => { diff --git a/src/test/pythonEnvironments/base/locators/composite/environmentsResolver.unit.test.ts b/src/test/pythonEnvironments/base/locators/composite/environmentsResolver.unit.test.ts index b0a7d5bbf17b..5c8e66b64f05 100644 --- a/src/test/pythonEnvironments/base/locators/composite/environmentsResolver.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/composite/environmentsResolver.unit.test.ts @@ -18,7 +18,7 @@ import { EnvironmentInfoService } from '../../../../../client/pythonEnvironments import { sleep } from '../../../../core'; import { createEnv, getEnvs, SimpleLocator } from '../../common'; -suite('Environments Resolver', () => { +suite('Python envs locator - Environments Resolver', () => { /** * Returns the expected environment to be returned by Environment info service */ @@ -94,8 +94,8 @@ suite('Environments Resolver', () => { // Assert const expectedUpdates = [ - { old: env1, new: createExpectedEnvInfo(env1) }, - { old: env2, new: createExpectedEnvInfo(env2) }, + { index: 0, old: env1, update: createExpectedEnvInfo(env1) }, + { index: 1, old: env2, update: createExpectedEnvInfo(env2) }, null, ]; assert.deepEqual(expectedUpdates, onUpdatedEvents); @@ -126,7 +126,7 @@ suite('Environments Resolver', () => { // Act await getEnvs(iterator); await sleep(1); - didUpdate.fire({ old: env, new: updatedEnv }); + didUpdate.fire({ index: 0, old: env, update: updatedEnv }); didUpdate.fire(null); // It is essential for the incoming iterator to fire "null" event signifying it's done await sleep(1); @@ -134,7 +134,7 @@ suite('Environments Resolver', () => { // The updates can be anything, even the number of updates, but they should lead to the same final state const { length } = onUpdatedEvents; assert.deepEqual( - onUpdatedEvents[length - 2]?.new, + onUpdatedEvents[length - 2]?.update, createExpectedEnvInfo(updatedEnv), 'The final update to environment is incorrect', ); From af6c7c5e2270d41ef3b09c508cab17072239ed6a Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 22 Sep 2020 18:56:36 -0600 Subject: [PATCH 08/25] Make parseVersion() a public helper. --- src/client/pythonEnvironments/base/info/pythonVersion.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/client/pythonEnvironments/base/info/pythonVersion.ts b/src/client/pythonEnvironments/base/info/pythonVersion.ts index 40e663c6231e..3532b01cf433 100644 --- a/src/client/pythonEnvironments/base/info/pythonVersion.ts +++ b/src/client/pythonEnvironments/base/info/pythonVersion.ts @@ -1,9 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import { EMPTY_VERSION, parseBasicVersionInfo } from '../../../../client/common/utils/version'; + import { PythonReleaseLevel, PythonVersion } from '.'; -import { EMPTY_VERSION, parseBasicVersionInfo } from '../../../common/utils/version'; +/** + * Convert the given string into the corresponding Python version object. + */ export function parseVersion(versionStr: string): PythonVersion { const parsed = parseBasicVersionInfo(versionStr); if (!parsed) { From e737607f71312908defec41c65cac4b96aa5650b Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 22 Sep 2020 20:06:00 -0600 Subject: [PATCH 09/25] buildEmptyEnvInfo() -> buildEnvInfo(). --- .../pythonEnvironments/base/info/env.ts | 26 ++++++- src/client/pythonEnvironments/legacyIOC.ts | 5 +- src/test/pythonEnvironments/base/common.ts | 67 +++++-------------- 3 files changed, 41 insertions(+), 57 deletions(-) diff --git a/src/client/pythonEnvironments/base/info/env.ts b/src/client/pythonEnvironments/base/info/env.ts index e73b5e23d154..8967b1b893be 100644 --- a/src/client/pythonEnvironments/base/info/env.ts +++ b/src/client/pythonEnvironments/base/info/env.ts @@ -18,9 +18,16 @@ import { /** * Create a new info object with all values empty. + * + * @param init - if provided, these values are applied to the new object */ -export function buildEmptyEnvInfo(): PythonEnvInfo { - return { +export function buildEnvInfo(init?: { + kind?: PythonEnvKind; + executable?: string; + location?: string; + version?: PythonVersion; +}): PythonEnvInfo { + const env = { kind: PythonEnvKind.Unknown, executable: { filename: '', @@ -44,6 +51,21 @@ export function buildEmptyEnvInfo(): PythonEnvInfo { org: '', }, }; + if (init !== undefined) { + if (init.kind !== undefined) { + env.kind = init.kind; + } + if (init.executable !== undefined) { + env.executable.filename = init.executable; + } + if (init.location !== undefined) { + env.location = init.location; + } + if (init.version !== undefined) { + env.version = init.version; + } + } + return env; } /** diff --git a/src/client/pythonEnvironments/legacyIOC.ts b/src/client/pythonEnvironments/legacyIOC.ts index 5675a0c3c8c1..f22995ff81f4 100644 --- a/src/client/pythonEnvironments/legacyIOC.ts +++ b/src/client/pythonEnvironments/legacyIOC.ts @@ -27,7 +27,7 @@ import { import { IPipEnvServiceHelper, IPythonInPathCommandProvider } from '../interpreter/locators/types'; import { IServiceContainer, IServiceManager } from '../ioc/types'; import { PythonEnvInfo, PythonEnvKind, PythonReleaseLevel } from './base/info'; -import { buildEmptyEnvInfo } from './base/info/env'; +import { buildEnvInfo } from './base/info/env'; import { ILocator, PythonLocatorQuery } from './base/locator'; import { getEnvs } from './base/locatorUtils'; import { initializeExternalDependencies } from './common/externalDependencies'; @@ -173,8 +173,7 @@ class ComponentAdapter implements IComponentAdapter { if (!this.enabled) { return undefined; } - const info = buildEmptyEnvInfo(); - info.executable.filename = pythonPath; + const info = buildEnvInfo({ executable: pythonPath }); if (resource !== undefined) { const wsFolder = vscode.workspace.getWorkspaceFolder(resource); if (wsFolder !== undefined) { diff --git a/src/test/pythonEnvironments/base/common.ts b/src/test/pythonEnvironments/base/common.ts index 9d44b161ef0b..aa0e579650a0 100644 --- a/src/test/pythonEnvironments/base/common.ts +++ b/src/test/pythonEnvironments/base/common.ts @@ -11,6 +11,7 @@ import { PythonEnvInfo, PythonEnvKind, } from '../../../client/pythonEnvironments/base/info'; +import { buildEnvInfo } from '../../../client/pythonEnvironments/base/info/env'; import { parseVersion } from '../../../client/pythonEnvironments/base/info/pythonVersion'; import { IPythonEnvsIterator, Locator, PythonEnvUpdatedEvent, PythonLocatorQuery, @@ -21,33 +22,6 @@ export function fixPath(filename: string): string { return path.normalize(filename); } -export function createEnv( - name: string, - versionStr: string, - kind?: PythonEnvKind, - execStr = 'python', -): PythonEnvInfo { - if (kind === undefined) { - kind = PythonEnvKind.Unknown; - } - const executable = execStr === '' ? 'python' : fixPath(execStr); - const version = parseVersion(versionStr); - return { - kind, - version, - name, - location: '', - arch: Architecture.x86, - executable: { - filename: executable, - sysPrefix: '', - mtime: -1, - ctime: -1 - }, - distro: { org: '' } - }; -} - export function createLocatedEnv( locationStr: string, versionStr: string, @@ -56,39 +30,28 @@ export function createLocatedEnv( ): PythonEnvInfo { const location = fixPath(locationStr); const normalizedExecutable = fixPath(execStr); - const executable = path.isAbsolute(normalizedExecutable) + const executable = location === '' || path.isAbsolute(normalizedExecutable) ? normalizedExecutable : path.join(location, 'bin', normalizedExecutable); - const env = createEnv('', versionStr, kind, executable); - env.location = location; + const version = parseVersion(versionStr); + const env = buildEnvInfo({ kind, executable, location, version }); + env.arch = Architecture.x86; return env; } -export function copyEnv( - env: PythonEnvInfo, - updates?: { - kind?: PythonEnvKind, - }, +export function createNamedEnv( + name: string, + versionStr: string, + kind?: PythonEnvKind, + execStr = 'python', ): PythonEnvInfo { - const copied = { ...env }; - copied.version = { ...env.version }; - copied.version.release = { ...env.version.release }; - copied.executable = { ...env.executable }; - copied.distro = { ...env.distro }; - if (copied.distro.version !== undefined) { - copied.distro.version = { ...env.distro.version! }; - } - // We don't bother with env.searchLocation. - - // Apply updates. - if (updates !== undefined) { - if (updates.kind !== undefined) { - copied.kind = updates.kind; - } - } - return copied; + const env = createLocatedEnv('', versionStr, kind, execStr); + env.name = name; + return env; } +export const createEnv = createLocatedEnv; + export class SimpleLocator extends Locator { private deferred = createDeferred(); constructor( From 51c6dee565043d823285b004283dfcb57e55a542 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 23 Sep 2020 12:15:31 -0600 Subject: [PATCH 10/25] Factor out updateEnv(). --- .../pythonEnvironments/base/info/env.ts | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/src/client/pythonEnvironments/base/info/env.ts b/src/client/pythonEnvironments/base/info/env.ts index 8967b1b893be..d6ae76ba9311 100644 --- a/src/client/pythonEnvironments/base/info/env.ts +++ b/src/client/pythonEnvironments/base/info/env.ts @@ -52,18 +52,7 @@ export function buildEnvInfo(init?: { }, }; if (init !== undefined) { - if (init.kind !== undefined) { - env.kind = init.kind; - } - if (init.executable !== undefined) { - env.executable.filename = init.executable; - } - if (init.location !== undefined) { - env.location = init.location; - } - if (init.version !== undefined) { - env.version = init.version; - } + updateEnv(env, init); } return env; } @@ -91,13 +80,31 @@ export function copyEnvInfo( // Apply updates. if (updates !== undefined) { - if (updates.kind !== undefined) { - copied.kind = updates.kind; - } + updateEnv(copied, updates); } return copied; } +function updateEnv(env: PythonEnvInfo, updates: { + kind?: PythonEnvKind; + executable?: string; + location?: string; + version?: PythonVersion; +}): void { + if (updates.kind !== undefined) { + env.kind = updates.kind; + } + if (updates.executable !== undefined) { + env.executable.filename = updates.executable; + } + if (updates.location !== undefined) { + env.location = updates.location; + } + if (updates.version !== undefined) { + env.version = updates.version; + } +} + /** * Checks if two environments are same. * @param {string | PythonEnvInfo} left: environment to compare. From 2ecd537309ab5bad9efdf8e21374aa27f92e2d1d Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 23 Sep 2020 12:20:51 -0600 Subject: [PATCH 11/25] Use lodash.cloneDeep() to deep-copy objects. --- src/client/pythonEnvironments/base/info/env.ts | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/client/pythonEnvironments/base/info/env.ts b/src/client/pythonEnvironments/base/info/env.ts index d6ae76ba9311..fdc556cbf5fe 100644 --- a/src/client/pythonEnvironments/base/info/env.ts +++ b/src/client/pythonEnvironments/base/info/env.ts @@ -68,17 +68,9 @@ export function copyEnvInfo( kind?: PythonEnvKind, }, ): PythonEnvInfo { - const copied = { ...env }; - copied.version = { ...env.version }; - copied.version.release = { ...env.version.release }; - copied.executable = { ...env.executable }; - copied.distro = { ...env.distro }; - if (copied.distro.version !== undefined) { - copied.distro.version = { ...env.distro.version! }; - } - // We don't bother with env.searchLocation. - - // Apply updates. + // We don't care whether or not extra/hidden properties + // get preserved, so we do the easy thing here. + const copied = cloneDeep(env); if (updates !== undefined) { updateEnv(copied, updates); } From 9cd6c7dde332bf82ce92cb657481faa8e805f863 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 23 Sep 2020 12:25:37 -0600 Subject: [PATCH 12/25] Drop the testing helper createEnv(). --- src/test/pythonEnvironments/base/common.ts | 4 +- .../base/locatorUtils.unit.test.ts | 18 +- .../base/locators.unit.test.ts | 50 +++--- .../environmentsReducer.unit.test.ts | 60 +++---- .../environmentsResolver.unit.test.ts | 28 ++-- .../discovery/locators/index.unit.test.ts | 155 +++++++++--------- 6 files changed, 158 insertions(+), 157 deletions(-) diff --git a/src/test/pythonEnvironments/base/common.ts b/src/test/pythonEnvironments/base/common.ts index aa0e579650a0..2c912b671e4b 100644 --- a/src/test/pythonEnvironments/base/common.ts +++ b/src/test/pythonEnvironments/base/common.ts @@ -50,8 +50,6 @@ export function createNamedEnv( return env; } -export const createEnv = createLocatedEnv; - export class SimpleLocator extends Locator { private deferred = createDeferred(); constructor( @@ -114,7 +112,7 @@ export class SimpleLocator extends Locator { return iterator; } public async resolveEnv(env: string | PythonEnvInfo): Promise { - const envInfo: PythonEnvInfo = typeof env === 'string' ? createEnv('', '', undefined, env) : env; + const envInfo: PythonEnvInfo = typeof env === 'string' ? createLocatedEnv('', '', undefined, env) : env; if (this.callbacks?.resolve === undefined) { return envInfo; } else if (this.callbacks?.resolve === null) { diff --git a/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts b/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts index 41b408761c87..e52523de76e2 100644 --- a/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts +++ b/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts @@ -14,8 +14,8 @@ import { } from '../../../client/pythonEnvironments/base/locator'; import { getEnvs, getQueryFilter } from '../../../client/pythonEnvironments/base/locatorUtils'; import { - createEnv, createLocatedEnv, + createNamedEnv, fixPath, } from './common'; @@ -30,21 +30,21 @@ function setSearchLocation(env: PythonEnvInfo, location?: string): void { env.searchLocation = Uri.file(locationStr); } -const env1 = createEnv('env1', '3.8', PythonEnvKind.System, '/usr/bin/python3.8'); -const env2 = createEnv('env2', '3.8.1rc2', PythonEnvKind.Pyenv, '/pyenv/3.8.1rc2/bin/python'); -const env3 = createEnv('env3', '3.9.1b2', PythonEnvKind.Unknown, 'python3.9'); -const env4 = createEnv('env4', '2.7.11', PythonEnvKind.Pyenv, '/pyenv/2.7.11/bin/python'); -const env5 = createEnv('env5', '2.7', PythonEnvKind.System, 'python2'); -const env6 = createEnv('env6', '3.7.4', PythonEnvKind.Conda, 'python'); +const env1 = createNamedEnv('env1', '3.8', PythonEnvKind.System, '/usr/bin/python3.8'); +const env2 = createNamedEnv('env2', '3.8.1rc2', PythonEnvKind.Pyenv, '/pyenv/3.8.1rc2/bin/python'); +const env3 = createNamedEnv('env3', '3.9.1b2', PythonEnvKind.Unknown, 'python3.9'); +const env4 = createNamedEnv('env4', '2.7.11', PythonEnvKind.Pyenv, '/pyenv/2.7.11/bin/python'); +const env5 = createNamedEnv('env5', '2.7', PythonEnvKind.System, 'python2'); +const env6 = createNamedEnv('env6', '3.7.4', PythonEnvKind.Conda, 'python'); const plainEnvs = [env1, env2, env3, env4, env5, env6]; const envL1 = createLocatedEnv('/.venvs/envL1', '3.9.0', PythonEnvKind.Venv); const envL2 = createLocatedEnv('/conda/envs/envL2', '3.8.3', PythonEnvKind.Conda); const locatedEnvs = [envL1, envL2]; -const envS1 = createEnv('env S1', '3.9', PythonEnvKind.OtherVirtual, `${homeDir}/some-dir/bin/python`); +const envS1 = createNamedEnv('env S1', '3.9', PythonEnvKind.OtherVirtual, `${homeDir}/some-dir/bin/python`); setSearchLocation(envS1, homeDir); -const envS2 = createEnv('env S2', '3.9', PythonEnvKind.OtherVirtual, `${homeDir}/some-dir2/bin/python`); +const envS2 = createNamedEnv('env S2', '3.9', PythonEnvKind.OtherVirtual, `${homeDir}/some-dir2/bin/python`); setSearchLocation(envS2, homeDir); const rootedEnvs = [envS1, envS2]; diff --git a/src/test/pythonEnvironments/base/locators.unit.test.ts b/src/test/pythonEnvironments/base/locators.unit.test.ts index d7500fc37a05..7621a9610a3d 100644 --- a/src/test/pythonEnvironments/base/locators.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators.unit.test.ts @@ -8,7 +8,7 @@ import { PythonEnvInfo, PythonEnvKind } from '../../../client/pythonEnvironments import { PythonLocatorQuery } from '../../../client/pythonEnvironments/base/locator'; import { DisableableLocator, Locators } from '../../../client/pythonEnvironments/base/locators'; import { PythonEnvsChangedEvent } from '../../../client/pythonEnvironments/base/watcher'; -import { createEnv, createLocatedEnv, getEnvs, SimpleLocator } from './common'; +import { createLocatedEnv, createNamedEnv, getEnvs, SimpleLocator } from './common'; suite('Python envs locators - Locators', () => { suite('onChanged consolidates', () => { @@ -63,7 +63,7 @@ suite('Python envs locators - Locators', () => { }); test('one', async () => { - const env1 = createEnv('foo', '3.8', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8', PythonEnvKind.Venv); const expected: PythonEnvInfo[] = [env1]; const sub1 = new SimpleLocator(expected); const locators = new Locators([sub1]); @@ -75,11 +75,11 @@ suite('Python envs locators - Locators', () => { }); test('many', async () => { - const env1 = createEnv('foo', '3.5.12b1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.5.12b1', PythonEnvKind.Venv); const env2 = createLocatedEnv('some-dir', '3.8.1', PythonEnvKind.Conda); - const env3 = createEnv('python2', '2.7', PythonEnvKind.System); - const env4 = createEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); - const env5 = createEnv('hello world', '3.8', PythonEnvKind.System); + const env3 = createNamedEnv('python2', '2.7', PythonEnvKind.System); + const env4 = createNamedEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); + const env5 = createNamedEnv('hello world', '3.8', PythonEnvKind.System); const expected = [env1, env2, env3, env4, env5]; const sub1 = new SimpleLocator([env1]); const sub2 = new SimpleLocator([], { before: sub1.done }); @@ -103,7 +103,7 @@ suite('Python envs locators - Locators', () => { query = q; return e; } - const env1 = createEnv('foo', '3.8', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8', PythonEnvKind.Venv); const sub1 = new SimpleLocator([env1], { onQuery }); const locators = new Locators([sub1]); @@ -114,13 +114,13 @@ suite('Python envs locators - Locators', () => { }); test('iterate out of order', async () => { - const env1 = createEnv('foo', '3.5.12b1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.5.12b1', PythonEnvKind.Venv); const env2 = createLocatedEnv('some-dir', '3.8.1', PythonEnvKind.Conda); - const env3 = createEnv('python2', '2.7', PythonEnvKind.System); - const env4 = createEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); - const env5 = createEnv('hello world', '3.8', PythonEnvKind.System); - const env6 = createEnv('spam', '3.10.0a0', PythonEnvKind.Custom); - const env7 = createEnv('eggs', '3.9.1a0', PythonEnvKind.Custom); + const env3 = createNamedEnv('python2', '2.7', PythonEnvKind.System); + const env4 = createNamedEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); + const env5 = createNamedEnv('hello world', '3.8', PythonEnvKind.System); + const env6 = createNamedEnv('spam', '3.10.0a0', PythonEnvKind.Custom); + const env7 = createNamedEnv('eggs', '3.9.1a0', PythonEnvKind.Custom); const expected = [env5, env1, env2, env3, env4, env6, env7]; const sub4 = new SimpleLocator([env5]); const sub2 = new SimpleLocator([env1], { before: sub4.done }); @@ -136,11 +136,11 @@ suite('Python envs locators - Locators', () => { }); test('iterate intermingled', async () => { - const env1 = createEnv('foo', '3.5.12b1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.5.12b1', PythonEnvKind.Venv); const env2 = createLocatedEnv('some-dir', '3.8.1', PythonEnvKind.Conda); - const env3 = createEnv('python2', '2.7', PythonEnvKind.System); - const env4 = createEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); - const env5 = createEnv('hello world', '3.8', PythonEnvKind.System); + const env3 = createNamedEnv('python2', '2.7', PythonEnvKind.System); + const env4 = createNamedEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); + const env5 = createNamedEnv('hello world', '3.8', PythonEnvKind.System); const expected = [env1, env4, env2, env5, env3]; const deferred1 = createDeferred(); const deferred2 = createDeferred(); @@ -189,7 +189,7 @@ suite('Python envs locators - Locators', () => { suite('resolveEnv()', () => { test('one wrapped', async () => { - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const expected = env1; const calls: number[] = []; const sub1 = new SimpleLocator([env1], { resolve: async (e) => { @@ -205,7 +205,7 @@ suite('Python envs locators - Locators', () => { }); test('first one resolves', async () => { - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const expected = env1; const calls: number[] = []; const sub1 = new SimpleLocator([env1], { resolve: async (e) => { @@ -225,7 +225,7 @@ suite('Python envs locators - Locators', () => { }); test('second one resolves', async () => { - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const expected = env1; const calls: number[] = []; const sub1 = new SimpleLocator([env1], { resolve: async (_e) => { @@ -245,7 +245,7 @@ suite('Python envs locators - Locators', () => { }); test('none resolve', async () => { - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const calls: number[] = []; const sub1 = new SimpleLocator([env1], { resolve: async (_e) => { calls.push(1); @@ -320,7 +320,7 @@ suite('Python envs locators - DisableableLocator', () => { suite('iterEnvs()', () => { test('pass-through if enabled', async () => { - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const expected = [env1]; const sub = new SimpleLocator([env1]); const locator = new DisableableLocator(sub); @@ -332,7 +332,7 @@ suite('Python envs locators - DisableableLocator', () => { }); test('empty if disabled', async () => { - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const expected: PythonEnvInfo[] = []; const sub = new SimpleLocator([env1]); const locator = new DisableableLocator(sub); @@ -347,7 +347,7 @@ suite('Python envs locators - DisableableLocator', () => { suite('resolveEnv()', () => { test('pass-through if enabled', async () => { - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const expected = env1; const sub = new SimpleLocator([env1]); const locator = new DisableableLocator(sub); @@ -358,7 +358,7 @@ suite('Python envs locators - DisableableLocator', () => { }); test('empty if disabled', async () => { - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const sub = new SimpleLocator([env1]); const locator = new DisableableLocator(sub); diff --git a/src/test/pythonEnvironments/base/locators/composite/environmentsReducer.unit.test.ts b/src/test/pythonEnvironments/base/locators/composite/environmentsReducer.unit.test.ts index 4977bdb53699..04434b21bc3b 100644 --- a/src/test/pythonEnvironments/base/locators/composite/environmentsReducer.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/composite/environmentsReducer.unit.test.ts @@ -13,16 +13,16 @@ import { } from '../../../../../client/pythonEnvironments/base/locators/composite/environmentsReducer'; import { PythonEnvsChangedEvent } from '../../../../../client/pythonEnvironments/base/watcher'; import { sleep } from '../../../../core'; -import { createEnv, getEnvs, SimpleLocator } from '../../common'; +import { createNamedEnv, getEnvs, SimpleLocator } from '../../common'; suite('Python envs locator - Environments Reducer', () => { suite('iterEnvs()', () => { test('Iterator only yields unique environments', async () => { - const env1 = createEnv('env1', '3.5', PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); - const env2 = createEnv('env2', '3.8', PythonEnvKind.Conda, path.join('path', 'to', 'exec2')); - const env3 = createEnv('env3', '2.7', PythonEnvKind.System, path.join('path', 'to', 'exec3')); - const env4 = createEnv('env4', '3.8.1', PythonEnvKind.Unknown, path.join('path', 'to', 'exec2')); // Same as env2 - const env5 = createEnv('env5', '3.5.12b1', PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); // Same as env1 + const env1 = createNamedEnv('env1', '3.5', PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); + const env2 = createNamedEnv('env2', '3.8', PythonEnvKind.Conda, path.join('path', 'to', 'exec2')); + const env3 = createNamedEnv('env3', '2.7', PythonEnvKind.System, path.join('path', 'to', 'exec3')); + const env4 = createNamedEnv('env4', '3.8.1', PythonEnvKind.Unknown, path.join('path', 'to', 'exec2')); // Same as env2 + const env5 = createNamedEnv('env5', '3.5.12b1', PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); // Same as env1 const environmentsToBeIterated = [env1, env2, env3, env4, env5]; // Contains 3 unique environments const parentLocator = new SimpleLocator(environmentsToBeIterated); const reducer = new PythonEnvsReducer(parentLocator); @@ -36,11 +36,11 @@ suite('Python envs locator - Environments Reducer', () => { test('Single updates for multiple environments are sent correctly followed by the null event', async () => { // Arrange - const env1 = createEnv('env1', '3.5', PythonEnvKind.Unknown, path.join('path', 'to', 'exec1')); - const env2 = createEnv('env2', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec2')); - const env3 = createEnv('env3', '2.7', PythonEnvKind.System, path.join('path', 'to', 'exec3')); - const env4 = createEnv('env4', '3.8.1', PythonEnvKind.Conda, path.join('path', 'to', 'exec2')); // Same as env2; - const env5 = createEnv('env5', '3.5.12b1', PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); // Same as env1; + const env1 = createNamedEnv('env1', '3.5', PythonEnvKind.Unknown, path.join('path', 'to', 'exec1')); + const env2 = createNamedEnv('env2', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec2')); + const env3 = createNamedEnv('env3', '2.7', PythonEnvKind.System, path.join('path', 'to', 'exec3')); + const env4 = createNamedEnv('env4', '3.8.1', PythonEnvKind.Conda, path.join('path', 'to', 'exec2')); // Same as env2; + const env5 = createNamedEnv('env5', '3.5.12b1', PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); // Same as env1; const environmentsToBeIterated = [env1, env2, env3, env4, env5]; // Contains 3 unique environments const parentLocator = new SimpleLocator(environmentsToBeIterated); const onUpdatedEvents: (PythonEnvUpdatedEvent | null)[] = []; @@ -73,9 +73,9 @@ suite('Python envs locator - Environments Reducer', () => { test('Multiple updates for the same environment are sent correctly followed by the null event', async () => { // Arrange - const env1 = createEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); - const env2 = createEnv('env2', '3.8.1', PythonEnvKind.System, path.join('path', 'to', 'exec')); - const env3 = createEnv('env3', '3.8.1', PythonEnvKind.Conda, path.join('path', 'to', 'exec')); + const env1 = createNamedEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); + const env2 = createNamedEnv('env2', '3.8.1', PythonEnvKind.System, path.join('path', 'to', 'exec')); + const env3 = createNamedEnv('env3', '3.8.1', PythonEnvKind.Conda, path.join('path', 'to', 'exec')); const environmentsToBeIterated = [env1, env2, env3]; // All refer to the same environment const parentLocator = new SimpleLocator(environmentsToBeIterated); const onUpdatedEvents: (PythonEnvUpdatedEvent | null)[] = []; @@ -118,8 +118,8 @@ suite('Python envs locator - Environments Reducer', () => { test('Updates to environments from the incoming iterator are passed on correctly followed by the null event', async () => { // Arrange - const env1 = createEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); - const env2 = createEnv('env2', '3.8.1', PythonEnvKind.System, path.join('path', 'to', 'exec')); + const env1 = createNamedEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); + const env2 = createNamedEnv('env2', '3.8.1', PythonEnvKind.System, path.join('path', 'to', 'exec')); const environmentsToBeIterated = [env1]; const didUpdate = new EventEmitter(); const parentLocator = new SimpleLocator(environmentsToBeIterated, { onUpdated: didUpdate.event }); @@ -172,17 +172,17 @@ suite('Python envs locator - Environments Reducer', () => { suite('resolveEnv()', () => { test('Iterates environments from the reducer to get resolved environment, then calls into locator manager to resolve environment further and return it', async () => { - const env1 = createEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); - const env2 = createEnv('env2', '2.7', PythonEnvKind.System, path.join('path', 'to', 'exec3')); - const env3 = createEnv('env3', '3.8.1', PythonEnvKind.Conda, path.join('path', 'to', 'exec')); - const env4 = createEnv('env4', '3.8.1', PythonEnvKind.Conda, path.join('path', 'to', 'exec2')); - const env5 = createEnv('env5', '3.5.12b1', PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); - const env6 = createEnv('env6', '3.8.1', PythonEnvKind.System, path.join('path', 'to', 'exec')); + const env1 = createNamedEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); + const env2 = createNamedEnv('env2', '2.7', PythonEnvKind.System, path.join('path', 'to', 'exec3')); + const env3 = createNamedEnv('env3', '3.8.1', PythonEnvKind.Conda, path.join('path', 'to', 'exec')); + const env4 = createNamedEnv('env4', '3.8.1', PythonEnvKind.Conda, path.join('path', 'to', 'exec2')); + const env5 = createNamedEnv('env5', '3.5.12b1', PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); + const env6 = createNamedEnv('env6', '3.8.1', PythonEnvKind.System, path.join('path', 'to', 'exec')); const environmentsToBeIterated = [env1, env2, env3, env4, env5, env6]; // env1 env3 env6 are same const env13 = mergeEnvironments(env1, env3); const env136 = mergeEnvironments(env13, env6); - const expected = createEnv('resolvedEnv', '3.8.1', PythonEnvKind.Conda, 'resolved/path/to/exec'); + const expected = createNamedEnv('resolvedEnv', '3.8.1', PythonEnvKind.Conda, 'resolved/path/to/exec'); const parentLocator = new SimpleLocator(environmentsToBeIterated, { resolve: async (e: PythonEnvInfo) => { if (isEqual(e, env136)) { @@ -200,12 +200,12 @@ suite('Python envs locator - Environments Reducer', () => { }); test("If the reducer isn't able to resolve environment, return undefined", async () => { - const env1 = createEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); - const env2 = createEnv('env2', '2.7', PythonEnvKind.System, path.join('path', 'to', 'exec3')); - const env3 = createEnv('env3', '3.8.1', PythonEnvKind.Conda, path.join('path', 'to', 'exec')); - const env4 = createEnv('env4', '3.8.1', PythonEnvKind.Conda, path.join('path', 'to', 'exec2')); - const env5 = createEnv('env5', '3.5.12b1', PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); - const env6 = createEnv('env6', '3.8.1', PythonEnvKind.System, path.join('path', 'to', 'exec')); + const env1 = createNamedEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); + const env2 = createNamedEnv('env2', '2.7', PythonEnvKind.System, path.join('path', 'to', 'exec3')); + const env3 = createNamedEnv('env3', '3.8.1', PythonEnvKind.Conda, path.join('path', 'to', 'exec')); + const env4 = createNamedEnv('env4', '3.8.1', PythonEnvKind.Conda, path.join('path', 'to', 'exec2')); + const env5 = createNamedEnv('env5', '3.5.12b1', PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); + const env6 = createNamedEnv('env6', '3.8.1', PythonEnvKind.System, path.join('path', 'to', 'exec')); const environmentsToBeIterated = [env1, env2, env3, env4, env5, env6]; // env1 env3 env6 are same const env13 = mergeEnvironments(env1, env3); @@ -213,7 +213,7 @@ suite('Python envs locator - Environments Reducer', () => { const parentLocator = new SimpleLocator(environmentsToBeIterated, { resolve: async (e: PythonEnvInfo) => { if (isEqual(e, env136)) { - return createEnv('resolvedEnv', '3.8.1', PythonEnvKind.Conda, 'resolved/path/to/exec'); + return createNamedEnv('resolvedEnv', '3.8.1', PythonEnvKind.Conda, 'resolved/path/to/exec'); } return undefined; }, diff --git a/src/test/pythonEnvironments/base/locators/composite/environmentsResolver.unit.test.ts b/src/test/pythonEnvironments/base/locators/composite/environmentsResolver.unit.test.ts index 5c8e66b64f05..d56468212826 100644 --- a/src/test/pythonEnvironments/base/locators/composite/environmentsResolver.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/composite/environmentsResolver.unit.test.ts @@ -16,7 +16,7 @@ import { PythonEnvsChangedEvent } from '../../../../../client/pythonEnvironments import * as ExternalDep from '../../../../../client/pythonEnvironments/common/externalDependencies'; import { EnvironmentInfoService } from '../../../../../client/pythonEnvironments/info/environmentInfoService'; import { sleep } from '../../../../core'; -import { createEnv, getEnvs, SimpleLocator } from '../../common'; +import { createNamedEnv, getEnvs, SimpleLocator } from '../../common'; suite('Python envs locator - Environments Resolver', () => { /** @@ -53,10 +53,10 @@ suite('Python envs locator - Environments Resolver', () => { }); test('Iterator yields environments as-is', async () => { - const env1 = createEnv('env1', '3.5.12b1', PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); - const env2 = createEnv('env2', '3.8.1', PythonEnvKind.Conda, path.join('path', 'to', 'exec2')); - const env3 = createEnv('env3', '2.7', PythonEnvKind.System, path.join('path', 'to', 'exec3')); - const env4 = createEnv('env4', '3.9.0rc2', PythonEnvKind.Unknown, path.join('path', 'to', 'exec2')); + const env1 = createNamedEnv('env1', '3.5.12b1', PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); + const env2 = createNamedEnv('env2', '3.8.1', PythonEnvKind.Conda, path.join('path', 'to', 'exec2')); + const env3 = createNamedEnv('env3', '2.7', PythonEnvKind.System, path.join('path', 'to', 'exec3')); + const env4 = createNamedEnv('env4', '3.9.0rc2', PythonEnvKind.Unknown, path.join('path', 'to', 'exec2')); const environmentsToBeIterated = [env1, env2, env3, env4]; const parentLocator = new SimpleLocator(environmentsToBeIterated); const resolver = new PythonEnvsResolver(parentLocator, new EnvironmentInfoService()); @@ -69,8 +69,8 @@ suite('Python envs locator - Environments Resolver', () => { test('Updates for environments are sent correctly followed by the null event', async () => { // Arrange - const env1 = createEnv('env1', '3.5.12b1', PythonEnvKind.Unknown, path.join('path', 'to', 'exec1')); - const env2 = createEnv('env2', '3.8.1', PythonEnvKind.Unknown, path.join('path', 'to', 'exec2')); + const env1 = createNamedEnv('env1', '3.5.12b1', PythonEnvKind.Unknown, path.join('path', 'to', 'exec1')); + const env2 = createNamedEnv('env2', '3.8.1', PythonEnvKind.Unknown, path.join('path', 'to', 'exec2')); const environmentsToBeIterated = [env1, env2]; const parentLocator = new SimpleLocator(environmentsToBeIterated); const onUpdatedEvents: (PythonEnvUpdatedEvent | null)[] = []; @@ -103,8 +103,8 @@ suite('Python envs locator - Environments Resolver', () => { test('Updates to environments from the incoming iterator are sent correctly followed by the null event', async () => { // Arrange - const env = createEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); - const updatedEnv = createEnv('env1', '3.8.1', PythonEnvKind.System, path.join('path', 'to', 'exec')); + const env = createNamedEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); + const updatedEnv = createNamedEnv('env1', '3.8.1', PythonEnvKind.System, path.join('path', 'to', 'exec')); const environmentsToBeIterated = [env]; const didUpdate = new EventEmitter(); const parentLocator = new SimpleLocator(environmentsToBeIterated, { onUpdated: didUpdate.event }); @@ -179,8 +179,8 @@ suite('Python envs locator - Environments Resolver', () => { }); test('Calls into parent locator to get resolved environment, then calls environnment service to resolve environment further and return it', async () => { - const env = createEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); - const resolvedEnvReturnedByReducer = createEnv( + const env = createNamedEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); + const resolvedEnvReturnedByReducer = createNamedEnv( 'env1', '3.8.1', PythonEnvKind.Conda, @@ -207,8 +207,8 @@ suite('Python envs locator - Environments Resolver', () => { reject(); }), ); - const env = createEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); - const resolvedEnvReturnedByReducer = createEnv( + const env = createNamedEnv('env1', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); + const resolvedEnvReturnedByReducer = createNamedEnv( 'env1', '3.8.1', PythonEnvKind.Conda, @@ -230,7 +230,7 @@ suite('Python envs locator - Environments Resolver', () => { }); test("If the parent locator isn't able to resolve environment, return undefined", async () => { - const env = createEnv('env', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); + const env = createNamedEnv('env', '3.8', PythonEnvKind.Unknown, path.join('path', 'to', 'exec')); const parentLocator = new SimpleLocator([], { resolve: async () => undefined, }); diff --git a/src/test/pythonEnvironments/discovery/locators/index.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/index.unit.test.ts index 2480aeed31d8..e99982856a02 100644 --- a/src/test/pythonEnvironments/discovery/locators/index.unit.test.ts +++ b/src/test/pythonEnvironments/discovery/locators/index.unit.test.ts @@ -36,7 +36,10 @@ import { } from '../../../../client/pythonEnvironments/discovery/locators'; import { EnvironmentType, PythonEnvironment } from '../../../../client/pythonEnvironments/info'; import { - createEnv, createLocatedEnv, getEnvs, SimpleLocator, + createLocatedEnv, + createNamedEnv, + getEnvs, + SimpleLocator, } from '../../base/common'; class WorkspaceFolders { @@ -97,7 +100,7 @@ suite('WorkspaceLocators', () => { suite('onChanged', () => { test('no roots', () => { const expected: PythonEnvsChangedEvent[] = []; - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const loc1 = new SimpleLocator([env1]); const locators = new WorkspaceLocators([ () => [loc1], @@ -115,7 +118,7 @@ suite('WorkspaceLocators', () => { test('no factories', () => { const expected: PythonEnvsChangedEvent[] = []; - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const loc1 = new SimpleLocator([env1]); const locators = new WorkspaceLocators([]); const folders = new WorkspaceFolders(['foo', 'bar']); @@ -146,7 +149,7 @@ suite('WorkspaceLocators', () => { const event4: PythonEnvsChangedEvent = { kind: PythonEnvKind.Venv }; const event5: PythonEnvsChangedEvent = { kind: PythonEnvKind.Pipenv }; const event6: PythonEnvsChangedEvent = { kind: PythonEnvKind.Conda }; - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const loc1 = new SimpleLocator([env1]); const loc2 = new SimpleLocator([]); const loc3 = new SimpleLocator([]); @@ -263,7 +266,7 @@ suite('WorkspaceLocators', () => { suite('iterEnvs()', () => { test('no roots', async () => { const expected: PythonEnvInfo[] = []; - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const loc1 = new SimpleLocator([env1]); const locators = new WorkspaceLocators([ () => [loc1], @@ -307,7 +310,7 @@ suite('WorkspaceLocators', () => { test('one not empty', async () => { const root1 = Uri.file('foo'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const expected: PythonEnvInfo[] = [env1]; const loc1 = new SimpleLocator([env1]); const locators = new WorkspaceLocators([ @@ -324,8 +327,8 @@ suite('WorkspaceLocators', () => { test('empty locator ignored', async () => { const root1 = Uri.file('foo'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); - const env2 = createEnv('python2', '2.7', PythonEnvKind.Pipenv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env2 = createNamedEnv('python2', '2.7', PythonEnvKind.Pipenv); const expected: PythonEnvInfo[] = [env1, env2]; const loc1 = new SimpleLocator([env1]); const loc2 = new SimpleLocator([], { before: loc1.done }); @@ -345,14 +348,14 @@ suite('WorkspaceLocators', () => { test('consolidates envs across roots', async () => { const root1 = Uri.file('foo'); const root2 = Uri.file('bar'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const env2 = createLocatedEnv('foo/some-dir', '3.8.1', PythonEnvKind.Conda); - const env3 = createEnv('python2', '2.7', PythonEnvKind.Pipenv); - const env4 = createEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); - const env5 = createEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); - const env6 = createEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); - const env7 = createEnv('eggs', '3.9.1a0', PythonEnvKind.Venv); - const env8 = createEnv('foo', '3.5.12b1', PythonEnvKind.Venv); + const env3 = createNamedEnv('python2', '2.7', PythonEnvKind.Pipenv); + const env4 = createNamedEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); + const env5 = createNamedEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); + const env6 = createNamedEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); + const env7 = createNamedEnv('eggs', '3.9.1a0', PythonEnvKind.Venv); + const env8 = createNamedEnv('foo', '3.5.12b1', PythonEnvKind.Venv); const expected: PythonEnvInfo[] = [env1, env2, env3, env4, env5, env6, env7, env8]; const loc1 = new SimpleLocator([env1, env2]); const loc2 = new SimpleLocator([env3, env4], { before: loc1.done }); @@ -374,10 +377,10 @@ suite('WorkspaceLocators', () => { test('query matches a root', async () => { const root1 = Uri.file('foo'); const root2 = Uri.file('bar'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); - const env2 = createEnv('python2', '2.7', PythonEnvKind.Pipenv); - const env3 = createEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); - const env4 = createEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env2 = createNamedEnv('python2', '2.7', PythonEnvKind.Pipenv); + const env3 = createNamedEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); + const env4 = createNamedEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); const expected: PythonEnvInfo[] = [env1, env2]; const loc1 = new SimpleLocator([env1]); const loc2 = new SimpleLocator([env2], { before: loc1.done }); @@ -399,10 +402,10 @@ suite('WorkspaceLocators', () => { test('query matches all roots', async () => { const root1 = Uri.file('foo'); const root2 = Uri.file('bar'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); - const env2 = createEnv('python2', '2.7', PythonEnvKind.Pipenv); - const env3 = createEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); - const env4 = createEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env2 = createNamedEnv('python2', '2.7', PythonEnvKind.Pipenv); + const env3 = createNamedEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); + const env4 = createNamedEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); const expected: PythonEnvInfo[] = [env1, env2, env3, env4]; const loc1 = new SimpleLocator([env1]); const loc2 = new SimpleLocator([env2], { before: loc1.done }); @@ -424,10 +427,10 @@ suite('WorkspaceLocators', () => { test('query does not match a root', async () => { const root1 = Uri.file('foo'); const root2 = Uri.file('bar'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); - const env2 = createEnv('python2', '2.7', PythonEnvKind.Pipenv); - const env3 = createEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); - const env4 = createEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env2 = createNamedEnv('python2', '2.7', PythonEnvKind.Pipenv); + const env3 = createNamedEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); + const env4 = createNamedEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); const loc1 = new SimpleLocator([env1]); const loc2 = new SimpleLocator([env2], { before: loc1.done }); const loc3 = new SimpleLocator([env3], { before: loc2.done }); @@ -448,10 +451,10 @@ suite('WorkspaceLocators', () => { test('query has no searchLocation', async () => { const root1 = Uri.file('foo'); const root2 = Uri.file('bar'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); - const env2 = createEnv('python2', '2.7', PythonEnvKind.Pipenv); - const env3 = createEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); - const env4 = createEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env2 = createNamedEnv('python2', '2.7', PythonEnvKind.Pipenv); + const env3 = createNamedEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); + const env4 = createNamedEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); const expected: PythonEnvInfo[] = [env1, env2, env3, env4]; const loc1 = new SimpleLocator([env1]); const loc2 = new SimpleLocator([env2], { before: loc1.done }); @@ -473,14 +476,14 @@ suite('WorkspaceLocators', () => { test('iterate out of order', async () => { const root1 = Uri.file('foo'); const root2 = Uri.file('bar'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const env2 = createLocatedEnv('foo/some-dir', '3.8.1', PythonEnvKind.Conda); - const env3 = createEnv('python2', '2.7', PythonEnvKind.Pipenv); - const env4 = createEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); - const env5 = createEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); - const env6 = createEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); - const env7 = createEnv('eggs', '3.9.1a0', PythonEnvKind.Venv); - const env8 = createEnv('foo', '3.5.12b1', PythonEnvKind.Venv); + const env3 = createNamedEnv('python2', '2.7', PythonEnvKind.Pipenv); + const env4 = createNamedEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); + const env5 = createNamedEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); + const env6 = createNamedEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); + const env7 = createNamedEnv('eggs', '3.9.1a0', PythonEnvKind.Venv); + const env8 = createNamedEnv('foo', '3.5.12b1', PythonEnvKind.Venv); const expected: PythonEnvInfo[] = [env5, env6, env1, env2, env3, env4, env7, env8]; const loc3 = new SimpleLocator([env5, env6]); const loc1 = new SimpleLocator([env1, env2], { before: loc3.done }); @@ -502,14 +505,14 @@ suite('WorkspaceLocators', () => { test('iterate intermingled', async () => { const root1 = Uri.file('foo'); const root2 = Uri.file('bar'); - const env1 = createEnv('foo-x', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo-x', '3.8.1', PythonEnvKind.Venv); const env2 = createLocatedEnv('foo/some-dir', '3.8.1', PythonEnvKind.Conda); - const env3 = createEnv('python2', '2.7', PythonEnvKind.Pipenv); - const env4 = createEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); - const env5 = createEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); - const env6 = createEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); - const env7 = createEnv('eggs', '3.9.1a0', PythonEnvKind.Venv); - const env8 = createEnv('foo-y', '3.5.12b1', PythonEnvKind.Venv); + const env3 = createNamedEnv('python2', '2.7', PythonEnvKind.Pipenv); + const env4 = createNamedEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); + const env5 = createNamedEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); + const env6 = createNamedEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); + const env7 = createNamedEnv('eggs', '3.9.1a0', PythonEnvKind.Venv); + const env8 = createNamedEnv('foo-y', '3.5.12b1', PythonEnvKind.Venv); const expected = [env3, env6, env1, env2, env8, env4, env5, env7]; const ordered = [env1, env2, env3, env4, env5, env6, env7, env8]; const deferreds = [ @@ -554,14 +557,14 @@ suite('WorkspaceLocators', () => { test('respects roots set during activation', async () => { const root1 = Uri.file('foo'); const root2 = Uri.file('bar'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const env2 = createLocatedEnv('foo/some-dir', '3.8.1', PythonEnvKind.Conda); - const env3 = createEnv('python2', '2.7', PythonEnvKind.Pipenv); - const env4 = createEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); - const env5 = createEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); - const env6 = createEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); - const env7 = createEnv('eggs', '3.9.1a0', PythonEnvKind.Venv); - const env8 = createEnv('foo', '3.5.12b1', PythonEnvKind.Venv); + const env3 = createNamedEnv('python2', '2.7', PythonEnvKind.Pipenv); + const env4 = createNamedEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); + const env5 = createNamedEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); + const env6 = createNamedEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); + const env7 = createNamedEnv('eggs', '3.9.1a0', PythonEnvKind.Venv); + const env8 = createNamedEnv('foo', '3.5.12b1', PythonEnvKind.Venv); const expected: PythonEnvInfo[] = [env1, env2, env3, env4, env5, env6, env7, env8]; const loc1 = new SimpleLocator([env1, env2]); const loc2 = new SimpleLocator([env3, env4], { before: loc1.done }); @@ -586,14 +589,14 @@ suite('WorkspaceLocators', () => { test('respects added roots', async () => { const root1 = Uri.file('foo'); const root2 = Uri.file('bar'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const env2 = createLocatedEnv('foo/some-dir', '3.8.1', PythonEnvKind.Conda); - const env3 = createEnv('python2', '2.7', PythonEnvKind.Pipenv); - const env4 = createEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); - const env5 = createEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); - const env6 = createEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); - const env7 = createEnv('eggs', '3.9.1a0', PythonEnvKind.Venv); - const env8 = createEnv('foo', '3.5.12b1', PythonEnvKind.Venv); + const env3 = createNamedEnv('python2', '2.7', PythonEnvKind.Pipenv); + const env4 = createNamedEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); + const env5 = createNamedEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); + const env6 = createNamedEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); + const env7 = createNamedEnv('eggs', '3.9.1a0', PythonEnvKind.Venv); + const env8 = createNamedEnv('foo', '3.5.12b1', PythonEnvKind.Venv); const expected: PythonEnvInfo[] = [env1, env2, env3, env4, env5, env6, env7, env8]; const loc1 = new SimpleLocator([env1, env2]); const loc2 = new SimpleLocator([env3, env4], { before: loc1.done }); @@ -620,14 +623,14 @@ suite('WorkspaceLocators', () => { test('ignores removed roots', async () => { const root1 = Uri.file('foo'); const root2 = Uri.file('bar'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const env2 = createLocatedEnv('foo/some-dir', '3.8.1', PythonEnvKind.Conda); - const env3 = createEnv('python2', '2.7', PythonEnvKind.Pipenv); - const env4 = createEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); - const env5 = createEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); - const env6 = createEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); - const env7 = createEnv('eggs', '3.9.1a0', PythonEnvKind.Venv); - const env8 = createEnv('foo', '3.5.12b1', PythonEnvKind.Venv); + const env3 = createNamedEnv('python2', '2.7', PythonEnvKind.Pipenv); + const env4 = createNamedEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); + const env5 = createNamedEnv('hello world', '3.8', PythonEnvKind.VirtualEnv); + const env6 = createNamedEnv('spam', '3.10.0a0', PythonEnvKind.OtherVirtual); + const env7 = createNamedEnv('eggs', '3.9.1a0', PythonEnvKind.Venv); + const env8 = createNamedEnv('foo', '3.5.12b1', PythonEnvKind.Venv); const expectedBefore = [env1, env2, env3, env4, env5, env6, env7, env8]; const expectedAfter = [env1, env2, env3, env4]; const loc1 = new SimpleLocator([env1, env2]); @@ -661,7 +664,7 @@ suite('WorkspaceLocators', () => { } test('no roots', async () => { - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const loc1 = new SimpleLocator([env1]); const locators = new WorkspaceLocators([ () => [loc1], @@ -675,7 +678,7 @@ suite('WorkspaceLocators', () => { }); test('no factories', async () => { - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const locators = new WorkspaceLocators([]); const folders = new WorkspaceFolders(['foo', 'bar']); locators.activate(folders); @@ -687,7 +690,7 @@ suite('WorkspaceLocators', () => { test('one locator, not resolved', async () => { const root1 = Uri.file('foo'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const loc1 = new SimpleLocator([env1], { resolve: null }); const locators = new WorkspaceLocators([ () => [loc1], @@ -702,7 +705,7 @@ suite('WorkspaceLocators', () => { test('one locator, resolved', async () => { const root1 = Uri.file('foo'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const expected = env1; const loc1 = new SimpleLocator([env1]); const locators = new WorkspaceLocators([ @@ -718,7 +721,7 @@ suite('WorkspaceLocators', () => { test('one root, first locator resolves', async () => { const root1 = Uri.file('foo'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const expected = env1; const seen: number[] = []; const loc1 = new SimpleLocator([env1], { resolve: getResolver(seen, 1) }); @@ -737,7 +740,7 @@ suite('WorkspaceLocators', () => { test('one root, second locator resolves', async () => { const root1 = Uri.file('foo'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const expected = env1; const seen: number[] = []; const loc1 = new SimpleLocator([env1], { resolve: getResolver(seen, 1, false) }); @@ -756,7 +759,7 @@ suite('WorkspaceLocators', () => { test('one root, not resolved', async () => { const root1 = Uri.file('foo'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const seen: number[] = []; const loc1 = new SimpleLocator([env1], { resolve: getResolver(seen, 1, false) }); const loc2 = new SimpleLocator([], { resolve: getResolver(seen, 2, false) }); @@ -775,7 +778,7 @@ suite('WorkspaceLocators', () => { test('many roots, no searchLocation, second root matches', async () => { const root1 = Uri.file('foo'); const root2 = Uri.file('bar'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); const expected = env1; const seen: number[] = []; const loc1 = new SimpleLocator([env1], { resolve: getResolver(seen, 1, false) }); @@ -795,7 +798,7 @@ suite('WorkspaceLocators', () => { test('many roots, searchLocation matches', async () => { const root1 = Uri.file('foo'); const root2 = Uri.file('bar'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); env1.searchLocation = root2; const expected = env1; const seen: number[] = []; @@ -816,7 +819,7 @@ suite('WorkspaceLocators', () => { test('many roots, searchLocation does not match', async () => { const root1 = Uri.file('foo'); const root2 = Uri.file('bar'); - const env1 = createEnv('foo', '3.8.1', PythonEnvKind.Venv); + const env1 = createNamedEnv('foo', '3.8.1', PythonEnvKind.Venv); env1.searchLocation = Uri.file('baz'); const expected = env1; const seen: number[] = []; From 6b509730e36ca866aea05f72197a2515d912df7b Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 23 Sep 2020 18:03:12 -0600 Subject: [PATCH 13/25] Factor out getURIFilter(). --- src/client/common/utils/misc.ts | 39 +++++++++++++++++++ .../discovery/locators/index.ts | 23 +++++------ 2 files changed, 48 insertions(+), 14 deletions(-) diff --git a/src/client/common/utils/misc.ts b/src/client/common/utils/misc.ts index 8c6783b78c9f..6c93714b9a58 100644 --- a/src/client/common/utils/misc.ts +++ b/src/client/common/utils/misc.ts @@ -130,6 +130,45 @@ export function isUri(resource?: Uri | any): resource is Uri { return typeof uri.path === 'string' && typeof uri.scheme === 'string'; } +/** + * Create a filter func that determine if the given URI and candidate match. + * + * The scheme must match, as well as path. + * + * @param checkParent - if `true`, match if the candidate is rooted under `uri` + * @param checkChild - if `true`, match if `uri` is rooted under the candidate + * @param checkExact - if `true`, match if the candidate matches `uri` exactly + */ +export function getURIFilter( + uri: Uri, + opts: { + checkParent?: boolean; + checkChild?: boolean; + checkExact?: boolean; + } = { checkExact: true } +): (u: Uri) => boolean { + const uriRoot = uri.path.endsWith('/') ? uri.path : '{uri.path}/'; + function filter(candidate: Uri): boolean { + if (candidate.scheme !== uri.scheme) { + return false; + } + if (opts.checkExact && candidate.path === uri.path) { + return true; + } + if (opts.checkParent && candidate.path.startsWith(uriRoot)) { + return true; + } + if (opts.checkChild) { + const candidateRoot = candidate.path.endsWith('/') ? candidate.path : '{candidate.path}/'; + if (uri.path.startsWith(candidateRoot)) { + return true; + } + } + return false; + } + return filter; +} + export function isNotebookCell(documentOrUri: TextDocument | Uri): boolean { const uri = isUri(documentOrUri) ? documentOrUri : documentOrUri.uri; return uri.scheme.includes(NotebookCellScheme); diff --git a/src/client/pythonEnvironments/discovery/locators/index.ts b/src/client/pythonEnvironments/discovery/locators/index.ts index 67f38e0fe769..fe3ef2f74523 100644 --- a/src/client/pythonEnvironments/discovery/locators/index.ts +++ b/src/client/pythonEnvironments/discovery/locators/index.ts @@ -9,6 +9,7 @@ import { traceDecorators } from '../../../common/logger'; import { IPlatformService } from '../../../common/platform/types'; import { IDisposableRegistry } from '../../../common/types'; import { createDeferred, Deferred } from '../../../common/utils/async'; +import { getURIFilter } from '../../../common/utils/misc'; import { OSType } from '../../../common/utils/platform'; import { CONDA_ENV_FILE_SERVICE, @@ -97,10 +98,17 @@ export class WorkspaceLocators extends Locator { } public iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator { + if (query?.searchLocations === null) { + // Workspace envs all have searchLocation, so there's nothing to do. + return NOOP_ITERATOR; + } const iterators = Object.keys(this.locators).map((key) => { if (query?.searchLocations) { const root = this.roots[key]; - if (!matchURI(root, ...query.searchLocations)) { + // Match any related search location. + const filter = getURIFilter(root, { checkParent: true, checkChild: true, checkExact: true }); + if (!query.searchLocations.some(filter)) { + // This workspace folder did not match the query, so skip it! return NOOP_ITERATOR; } } @@ -168,19 +176,6 @@ export class WorkspaceLocators extends Locator { } } -/** - * Determine if the given URI matches one of the candidates. - * - * The scheme must match, as well as path. The path must match exactly - * or the URI must be a parent of one of the candidates. - */ -function matchURI(uri: Uri, ...candidates: Uri[]): boolean { - const uriPath = uri.path.endsWith('/') ? uri.path : '{uri.path}/'; - const matchedUri = candidates.find((candidate) => (candidate.scheme === uri.scheme) - && (candidate.path === uri.path || candidate.path.startsWith(uriPath))); - return matchedUri !== undefined; -} - // The parts of IComponentAdapter used here. interface IComponent { hasInterpreters: Promise; From ad8f195ac9384f9c2d69a0def5e295625e6c4825 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 23 Sep 2020 18:05:22 -0600 Subject: [PATCH 14/25] Allow locator queries to mix search locations and non. --- src/client/pythonEnvironments/base/locator.ts | 5 +- .../pythonEnvironments/base/locatorUtils.ts | 40 +++++++++---- .../discovery/locators/index.ts | 4 +- .../base/locatorUtils.unit.test.ts | 59 ++++++++++++++++++- 4 files changed, 92 insertions(+), 16 deletions(-) diff --git a/src/client/pythonEnvironments/base/locator.ts b/src/client/pythonEnvironments/base/locator.ts index 050a8b0afd35..bac7f4aade7b 100644 --- a/src/client/pythonEnvironments/base/locator.ts +++ b/src/client/pythonEnvironments/base/locator.ts @@ -87,10 +87,11 @@ export type BasicPythonLocatorQuery = { * emitted by watchers. * * @prop - searchLocations - if provided, results should be limited to - * within these locations + * within these locations; `null` means "include envs that have + * no search location" */ export type PythonLocatorQuery = BasicPythonLocatorQuery & { - searchLocations?: Uri[]; + searchLocations?: (Uri | null)[] | null; }; type QueryForEvent = E extends PythonEnvsChangedEvent ? PythonLocatorQuery : BasicPythonLocatorQuery; diff --git a/src/client/pythonEnvironments/base/locatorUtils.ts b/src/client/pythonEnvironments/base/locatorUtils.ts index aa086278a801..21ddd9f68dd4 100644 --- a/src/client/pythonEnvironments/base/locatorUtils.ts +++ b/src/client/pythonEnvironments/base/locatorUtils.ts @@ -1,7 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import { Uri } from 'vscode'; import { createDeferred } from '../../common/utils/async'; +import { getURIFilter } from '../../common/utils/misc'; import { PythonEnvInfo } from './info'; import { IPythonEnvsIterator, @@ -13,24 +15,40 @@ import { * Create a filter function to match the given query. */ export function getQueryFilter(query: PythonLocatorQuery): (env: PythonEnvInfo) => boolean { - const locations = (query.searchLocations !== undefined && query.searchLocations.length > 0) - ? query.searchLocations - .filter((loc) => loc !== undefined) - .map((loc) => loc.toString()) + const kinds = (query.kinds !== undefined && query.kinds.length > 0) + ? query.kinds : undefined; - + let includeGlobal = !query.searchLocations || query.searchLocations.length === 0; + let locationFilters: ((u: Uri) => boolean)[] | undefined; + if (!includeGlobal) { + const candidates = query.searchLocations!.filter((u) => !!u); + includeGlobal = candidates.length < query.searchLocations!.length; + if (candidates.length > 0) { + locationFilters = candidates.map((loc) => getURIFilter(loc!, { + checkExact: true, + })); + } + } return (env) => { - if (query.kinds !== undefined && query.kinds.length > 0) { - if (!query.kinds.includes(env.kind)) { + if (kinds !== undefined) { + if (!kinds.includes(env.kind)) { return false; } } - if (locations !== undefined) { - if (env.searchLocation === undefined) { + if (env.searchLocation === undefined) { + if (!includeGlobal) { + return false; + } + } else if (locationFilters === undefined) { + if (query.searchLocations === null) { + return false; + } + if (query.searchLocations && query.searchLocations.length > 0) { + // Only envs without searchLocation set were requested? return false; } - const loc = env.searchLocation.toString(); - if (!locations.some((l) => l === loc)) { + } else { + if (!locationFilters.some((filter) => filter(env.searchLocation!))) { return false; } } diff --git a/src/client/pythonEnvironments/discovery/locators/index.ts b/src/client/pythonEnvironments/discovery/locators/index.ts index fe3ef2f74523..da51104fbf08 100644 --- a/src/client/pythonEnvironments/discovery/locators/index.ts +++ b/src/client/pythonEnvironments/discovery/locators/index.ts @@ -107,7 +107,9 @@ export class WorkspaceLocators extends Locator { const root = this.roots[key]; // Match any related search location. const filter = getURIFilter(root, { checkParent: true, checkChild: true, checkExact: true }); - if (!query.searchLocations.some(filter)) { + // Ignore any requests for global envs. + const candidates: unknown = query.searchLocations.filter((u) => u !== null); + if (!(candidates as Uri[]).some(filter)) { // This workspace folder did not match the query, so skip it! return NOOP_ITERATOR; } diff --git a/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts b/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts index e52523de76e2..f28e0782dd65 100644 --- a/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts +++ b/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts @@ -191,7 +191,19 @@ suite('Python envs locator utils - getQueryFilter', () => { assert.deepEqual(filtered, expected); }); - test('match all', () => { + test('match multiple (include non-searched envs)', () => { + const expected = [...plainEnvs, ...locatedEnvs, ...rootedLocatedEnvs]; + const searchLocations = [null, ...rootedLocatedEnvs.map((env) => env.searchLocation!)]; + searchLocations.push(doesNotExist); + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match all searched', () => { const expected = [...rootedEnvs, ...rootedLocatedEnvs]; const searchLocations = expected.map((env) => env.searchLocation!); const query: PythonLocatorQuery = { searchLocations }; @@ -201,6 +213,50 @@ suite('Python envs locator utils - getQueryFilter', () => { assert.deepEqual(filtered, expected); }); + + test('match all (including non-searched)', () => { + const expected = envs; + const searchLocations = [null, ...expected.map((env) => env.searchLocation!)]; + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match only non-searched envs (null location)', () => { + const expected = [...plainEnvs, ...locatedEnvs]; + const searchLocations = [null]; + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match only non-searched envs (with unmatched location)', () => { + const expected = [...plainEnvs, ...locatedEnvs]; + const searchLocations: (Uri | null)[] = [null]; + searchLocations.push(doesNotExist); + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match only non-searched envs (explicit null)', () => { + const expected = [...plainEnvs, ...locatedEnvs]; + const query: PythonLocatorQuery = { searchLocations: null }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); }); suite('mixed query', () => { @@ -358,7 +414,6 @@ suite('Python envs locator utils - getEnvs', () => { }); test('yield many, all updated', async () => { - return; const expected = rootedLocatedEnvs; const emitter = new EventEmitter(); const iterator = (async function* () { From 2b70a1c86bb40fbe0c27552b55bf2b4c22ace445 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Wed, 23 Sep 2020 18:06:30 -0600 Subject: [PATCH 15/25] Match subdirectories in locator queries. --- src/client/common/utils/misc.ts | 2 +- .../pythonEnvironments/base/locatorUtils.ts | 1 + .../base/locatorUtils.unit.test.ts | 23 +++++++++++++++---- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/client/common/utils/misc.ts b/src/client/common/utils/misc.ts index 6c93714b9a58..74fd6efb4d4b 100644 --- a/src/client/common/utils/misc.ts +++ b/src/client/common/utils/misc.ts @@ -147,7 +147,7 @@ export function getURIFilter( checkExact?: boolean; } = { checkExact: true } ): (u: Uri) => boolean { - const uriRoot = uri.path.endsWith('/') ? uri.path : '{uri.path}/'; + const uriRoot = uri.path.endsWith('/') ? uri.path : `${uri.path}/`; function filter(candidate: Uri): boolean { if (candidate.scheme !== uri.scheme) { return false; diff --git a/src/client/pythonEnvironments/base/locatorUtils.ts b/src/client/pythonEnvironments/base/locatorUtils.ts index 21ddd9f68dd4..cfc16f274842 100644 --- a/src/client/pythonEnvironments/base/locatorUtils.ts +++ b/src/client/pythonEnvironments/base/locatorUtils.ts @@ -25,6 +25,7 @@ export function getQueryFilter(query: PythonLocatorQuery): (env: PythonEnvInfo) includeGlobal = candidates.length < query.searchLocations!.length; if (candidates.length > 0) { locationFilters = candidates.map((loc) => getURIFilter(loc!, { + checkParent: true, checkExact: true, })); } diff --git a/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts b/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts index f28e0782dd65..6acef9867d81 100644 --- a/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts +++ b/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts @@ -46,7 +46,9 @@ const envS1 = createNamedEnv('env S1', '3.9', PythonEnvKind.OtherVirtual, `${hom setSearchLocation(envS1, homeDir); const envS2 = createNamedEnv('env S2', '3.9', PythonEnvKind.OtherVirtual, `${homeDir}/some-dir2/bin/python`); setSearchLocation(envS2, homeDir); -const rootedEnvs = [envS1, envS2]; +const envS3 = createNamedEnv('env S2', '3.9', PythonEnvKind.OtherVirtual, `${workspaceRoot.fsPath}/p/python`); +envS3.searchLocation = workspaceRoot; +const rootedEnvs = [envS1, envS2, envS3]; const envSL1 = createLocatedEnv(`${homeDir}/.venvs/envSL1`, '3.9.0', PythonEnvKind.Venv); setSearchLocation(envSL1); @@ -167,9 +169,9 @@ suite('Python envs locator utils - getQueryFilter', () => { }); test('match multiple (one location)', () => { - const expected = [envS1, envS2]; + const expected = [envS3, envSL2]; const searchLocations = [ - envS1.searchLocation!, + workspaceRoot, ]; const query: PythonLocatorQuery = { searchLocations }; @@ -180,7 +182,7 @@ suite('Python envs locator utils - getQueryFilter', () => { }); test('match multiple (multiple locations)', () => { - const expected = rootedLocatedEnvs; + const expected = [envS3, ...rootedLocatedEnvs]; const searchLocations = rootedLocatedEnvs.map((env) => env.searchLocation!); searchLocations.push(doesNotExist); const query: PythonLocatorQuery = { searchLocations }; @@ -192,7 +194,7 @@ suite('Python envs locator utils - getQueryFilter', () => { }); test('match multiple (include non-searched envs)', () => { - const expected = [...plainEnvs, ...locatedEnvs, ...rootedLocatedEnvs]; + const expected = [...plainEnvs, ...locatedEnvs, envS3, ...rootedLocatedEnvs]; const searchLocations = [null, ...rootedLocatedEnvs.map((env) => env.searchLocation!)]; searchLocations.push(doesNotExist); const query: PythonLocatorQuery = { searchLocations }; @@ -225,6 +227,17 @@ suite('Python envs locator utils - getQueryFilter', () => { assert.deepEqual(filtered, expected); }); + test('match all searched under one root', () => { + const expected = [envS1, envS2, envSL1, envSL3, envSL5]; + const searchLocations = [Uri.file(homeDir)]; + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + test('match only non-searched envs (null location)', () => { const expected = [...plainEnvs, ...locatedEnvs]; const searchLocations = [null]; From 7b1953880205e8e6d72d440291c7d67df63dc670 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Thu, 24 Sep 2020 15:53:24 -0600 Subject: [PATCH 16/25] Make PythonVersion.release optional. --- .../pythonEnvironments/base/info/index.ts | 2 +- src/client/pythonEnvironments/legacyIOC.ts | 20 ++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/client/pythonEnvironments/base/info/index.ts b/src/client/pythonEnvironments/base/info/index.ts index ced0f31390e5..4418860a12e5 100644 --- a/src/client/pythonEnvironments/base/info/index.ts +++ b/src/client/pythonEnvironments/base/info/index.ts @@ -98,7 +98,7 @@ export type PythonVersionRelease = { * @prop sysVersion - the raw text from `sys.version` */ export type PythonVersion = BasicVersionInfo & { - release: PythonVersionRelease; + release?: PythonVersionRelease; sysVersion?: string; }; diff --git a/src/client/pythonEnvironments/legacyIOC.ts b/src/client/pythonEnvironments/legacyIOC.ts index f22995ff81f4..6c694451b1f7 100644 --- a/src/client/pythonEnvironments/legacyIOC.ts +++ b/src/client/pythonEnvironments/legacyIOC.ts @@ -106,13 +106,19 @@ function convertEnvInfo(info: PythonEnvInfo): PythonEnvironment { if (version !== undefined) { const { release, sysVersion } = version; - const { level, serial } = release; - const releaseStr = level === PythonReleaseLevel.Final - ? 'final' - : `${level}${serial}`; - const versionStr = `${getVersionString(version)}-${releaseStr}`; - env.version = parseVersion(versionStr); - env.sysVersion = sysVersion; + if (release === undefined) { + const versionStr = `${getVersionString(version)}-final`; + env.version = parseVersion(versionStr); + env.sysVersion = ''; + } else { + const { level, serial } = release; + const releaseStr = level === PythonReleaseLevel.Final + ? 'final' + : `${level}${serial}`; + const versionStr = `${getVersionString(version)}-${releaseStr}`; + env.version = parseVersion(versionStr); + env.sysVersion = sysVersion; + } } if (distro !== undefined && distro.org !== '') { From eb746f94795e48fb927005fd440cfa5559ad3c36 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Thu, 24 Sep 2020 15:56:45 -0600 Subject: [PATCH 17/25] Drop the fixPath() testing helper. --- src/test/pythonEnvironments/base/common.ts | 8 ++------ .../pythonEnvironments/base/locatorUtils.unit.test.ts | 7 +++---- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/test/pythonEnvironments/base/common.ts b/src/test/pythonEnvironments/base/common.ts index 2c912b671e4b..62077e46aa20 100644 --- a/src/test/pythonEnvironments/base/common.ts +++ b/src/test/pythonEnvironments/base/common.ts @@ -18,18 +18,14 @@ import { } from '../../../client/pythonEnvironments/base/locator'; import { PythonEnvsChangedEvent } from '../../../client/pythonEnvironments/base/watcher'; -export function fixPath(filename: string): string { - return path.normalize(filename); -} - export function createLocatedEnv( locationStr: string, versionStr: string, kind = PythonEnvKind.Unknown, execStr = 'python', ): PythonEnvInfo { - const location = fixPath(locationStr); - const normalizedExecutable = fixPath(execStr); + const location = locationStr === '' ? '' : path.normalize(locationStr); + const normalizedExecutable = path.normalize(execStr); const executable = location === '' || path.isAbsolute(normalizedExecutable) ? normalizedExecutable : path.join(location, 'bin', normalizedExecutable); diff --git a/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts b/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts index 6acef9867d81..665250b54f5c 100644 --- a/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts +++ b/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts @@ -16,17 +16,16 @@ import { getEnvs, getQueryFilter } from '../../../client/pythonEnvironments/base import { createLocatedEnv, createNamedEnv, - fixPath, } from './common'; -const homeDir = fixPath('/home/me'); +const homeDir = path.normalize('/home/me'); const workspaceRoot = Uri.file('workspace-root'); -const doesNotExist = Uri.file(fixPath('does-not-exist')); +const doesNotExist = Uri.file(path.normalize('does-not-exist')); function setSearchLocation(env: PythonEnvInfo, location?: string): void { const locationStr = location === undefined ? path.dirname(env.location) - : fixPath(location); + : path.normalize(location); env.searchLocation = Uri.file(locationStr); } From 20c46daceae752dcd121ef6e5d84446b170e4ab6 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Thu, 24 Sep 2020 16:14:50 -0600 Subject: [PATCH 18/25] lint --- .../base/info/pythonVersion.ts | 2 +- .../pythonEnvironments/base/locatorUtils.ts | 42 +++++++++++-------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/client/pythonEnvironments/base/info/pythonVersion.ts b/src/client/pythonEnvironments/base/info/pythonVersion.ts index 3532b01cf433..0e281eb4662c 100644 --- a/src/client/pythonEnvironments/base/info/pythonVersion.ts +++ b/src/client/pythonEnvironments/base/info/pythonVersion.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { EMPTY_VERSION, parseBasicVersionInfo } from '../../../../client/common/utils/version'; +import { EMPTY_VERSION, parseBasicVersionInfo } from '../../../common/utils/version'; import { PythonReleaseLevel, PythonVersion } from '.'; diff --git a/src/client/pythonEnvironments/base/locatorUtils.ts b/src/client/pythonEnvironments/base/locatorUtils.ts index cfc16f274842..57f0794b90b3 100644 --- a/src/client/pythonEnvironments/base/locatorUtils.ts +++ b/src/client/pythonEnvironments/base/locatorUtils.ts @@ -30,28 +30,36 @@ export function getQueryFilter(query: PythonLocatorQuery): (env: PythonEnvInfo) })); } } - return (env) => { - if (kinds !== undefined) { - if (!kinds.includes(env.kind)) { - return false; - } + function checkKind(env: PythonEnvInfo): boolean { + if (kinds === undefined) { + return true; } + return kinds.includes(env.kind); + } + function checkSearchLocation(env: PythonEnvInfo): boolean { if (env.searchLocation === undefined) { - if (!includeGlobal) { - return false; - } - } else if (locationFilters === undefined) { + // It is not a "rooted" env. + return includeGlobal; + } + + // It is a "rooted" env. + if (locationFilters === undefined) { if (query.searchLocations === null) { + // The caller explicitly refused rooted envs. return false; } - if (query.searchLocations && query.searchLocations.length > 0) { - // Only envs without searchLocation set were requested? - return false; - } - } else { - if (!locationFilters.some((filter) => filter(env.searchLocation!))) { - return false; - } + // `query.searchLocations` only had `null` in it. + return !query.searchLocations || query.searchLocations.length === 0; + } + // Check against the requested roots. + return locationFilters.some((filter) => filter(env.searchLocation!)); + } + return (env) => { + if (!checkKind(env)) { + return false; + } + if (!checkSearchLocation(env)) { + return false; } return true; }; From 9d970f565b2aca69922ee1745941447880d18191 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Mon, 28 Sep 2020 10:18:40 -0600 Subject: [PATCH 19/25] Clarify what a missing query field means. --- src/client/pythonEnvironments/base/locator.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/client/pythonEnvironments/base/locator.ts b/src/client/pythonEnvironments/base/locator.ts index bac7f4aade7b..af139aa637ed 100644 --- a/src/client/pythonEnvironments/base/locator.ts +++ b/src/client/pythonEnvironments/base/locator.ts @@ -74,7 +74,9 @@ export const NOOP_ITERATOR: IPythonEnvsIterator = iterEmpty(); * This is directly correlated with the `BasicPythonEnvsChangedEvent` * emitted by watchers. * - * @prop kinds - if provided, results should be limited to these env kinds + * @prop kinds - if provided, results should be limited to these env + * kinds; if not provided, the kind of each evnironment + * is not considered when filtering */ export type BasicPythonLocatorQuery = { kinds?: PythonEnvKind[]; @@ -88,7 +90,8 @@ export type BasicPythonLocatorQuery = { * * @prop - searchLocations - if provided, results should be limited to * within these locations; `null` means "include envs that have - * no search location" + * no search location"; if not provided, the search location + * of each enviroment is not considered when filtering */ export type PythonLocatorQuery = BasicPythonLocatorQuery & { searchLocations?: (Uri | null)[] | null; From ce53e541fbaf819dcadb90cd99c633273abd55f5 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Mon, 28 Sep 2020 13:23:20 -0600 Subject: [PATCH 20/25] Make PythonLocatorQuery.searchLocations more explicit about the non-rooted cases. --- src/client/pythonEnvironments/base/locator.ts | 24 +++-- .../pythonEnvironments/base/locatorUtils.ts | 51 +++++----- .../discovery/locators/index.ts | 5 +- src/client/pythonEnvironments/legacyIOC.ts | 2 +- .../base/locatorUtils.unit.test.ts | 92 +++++++++++-------- .../base/locators.unit.test.ts | 2 +- .../discovery/locators/index.unit.test.ts | 9 +- 7 files changed, 113 insertions(+), 72 deletions(-) diff --git a/src/client/pythonEnvironments/base/locator.ts b/src/client/pythonEnvironments/base/locator.ts index af139aa637ed..a1b372fb6988 100644 --- a/src/client/pythonEnvironments/base/locator.ts +++ b/src/client/pythonEnvironments/base/locator.ts @@ -82,19 +82,31 @@ export type BasicPythonLocatorQuery = { kinds?: PythonEnvKind[]; }; +/** + * The portion of a query related to env search locations. + */ +export type SearchLocations = { + /** + * The locations under which to look for environments. + */ + roots: Uri[]; + /** + * If true, also look for environments that do not have a search location. + */ + includeNonRooted?: boolean; +}; + /** * The full set of possible info to send to a locator when requesting environments. * * This is directly correlated with the `PythonEnvsChangedEvent` * emitted by watchers. - * - * @prop - searchLocations - if provided, results should be limited to - * within these locations; `null` means "include envs that have - * no search location"; if not provided, the search location - * of each enviroment is not considered when filtering */ export type PythonLocatorQuery = BasicPythonLocatorQuery & { - searchLocations?: (Uri | null)[] | null; + /** + * If provided, results should be limited to within these locations. + */ + searchLocations?: SearchLocations; }; type QueryForEvent = E extends PythonEnvsChangedEvent ? PythonLocatorQuery : BasicPythonLocatorQuery; diff --git a/src/client/pythonEnvironments/base/locatorUtils.ts b/src/client/pythonEnvironments/base/locatorUtils.ts index 57f0794b90b3..36528b42fb72 100644 --- a/src/client/pythonEnvironments/base/locatorUtils.ts +++ b/src/client/pythonEnvironments/base/locatorUtils.ts @@ -18,18 +18,16 @@ export function getQueryFilter(query: PythonLocatorQuery): (env: PythonEnvInfo) const kinds = (query.kinds !== undefined && query.kinds.length > 0) ? query.kinds : undefined; - let includeGlobal = !query.searchLocations || query.searchLocations.length === 0; - let locationFilters: ((u: Uri) => boolean)[] | undefined; - if (!includeGlobal) { - const candidates = query.searchLocations!.filter((u) => !!u); - includeGlobal = candidates.length < query.searchLocations!.length; - if (candidates.length > 0) { - locationFilters = candidates.map((loc) => getURIFilter(loc!, { - checkParent: true, - checkExact: true, - })); + let includeNonRooted = true; + if (query.searchLocations !== undefined) { + if (query.searchLocations.includeNonRooted !== undefined) { + includeNonRooted = query.searchLocations.includeNonRooted; + } else { + // We default to `false`. + includeNonRooted = false; } } + const locationFilters = getSearchLocationFilters(query); function checkKind(env: PythonEnvInfo): boolean { if (kinds === undefined) { return true; @@ -39,20 +37,15 @@ export function getQueryFilter(query: PythonLocatorQuery): (env: PythonEnvInfo) function checkSearchLocation(env: PythonEnvInfo): boolean { if (env.searchLocation === undefined) { // It is not a "rooted" env. - return includeGlobal; - } - - // It is a "rooted" env. - if (locationFilters === undefined) { - if (query.searchLocations === null) { - // The caller explicitly refused rooted envs. - return false; + return includeNonRooted; + } else { + // It is a "rooted" env. + if (locationFilters === undefined) { + return query.searchLocations === undefined; } - // `query.searchLocations` only had `null` in it. - return !query.searchLocations || query.searchLocations.length === 0; + // Check against the requested roots. + return locationFilters.some((filter) => filter(env.searchLocation!)); } - // Check against the requested roots. - return locationFilters.some((filter) => filter(env.searchLocation!)); } return (env) => { if (!checkKind(env)) { @@ -65,6 +58,20 @@ export function getQueryFilter(query: PythonLocatorQuery): (env: PythonEnvInfo) }; } +function getSearchLocationFilters(query: PythonLocatorQuery): ((u: Uri) => boolean)[] | undefined { + if (query.searchLocations === undefined) { + return undefined; + } + const candidates: Uri[] = query.searchLocations.roots.filter((u) => !!u); + if (candidates.length === 0) { + return undefined; + } + return candidates.map((loc) => getURIFilter(loc, { + checkParent: true, + checkExact: true, + })); +} + /** * Unroll the given iterator into an array. * diff --git a/src/client/pythonEnvironments/discovery/locators/index.ts b/src/client/pythonEnvironments/discovery/locators/index.ts index da51104fbf08..49bec156a39b 100644 --- a/src/client/pythonEnvironments/discovery/locators/index.ts +++ b/src/client/pythonEnvironments/discovery/locators/index.ts @@ -103,13 +103,12 @@ export class WorkspaceLocators extends Locator { return NOOP_ITERATOR; } const iterators = Object.keys(this.locators).map((key) => { - if (query?.searchLocations) { + if (query?.searchLocations !== undefined) { const root = this.roots[key]; // Match any related search location. const filter = getURIFilter(root, { checkParent: true, checkChild: true, checkExact: true }); // Ignore any requests for global envs. - const candidates: unknown = query.searchLocations.filter((u) => u !== null); - if (!(candidates as Uri[]).some(filter)) { + if (!query.searchLocations.roots.some(filter)) { // This workspace folder did not match the query, so skip it! return NOOP_ITERATOR; } diff --git a/src/client/pythonEnvironments/legacyIOC.ts b/src/client/pythonEnvironments/legacyIOC.ts index 6c694451b1f7..d6c199d9e301 100644 --- a/src/client/pythonEnvironments/legacyIOC.ts +++ b/src/client/pythonEnvironments/legacyIOC.ts @@ -267,7 +267,7 @@ class ComponentAdapter implements IComponentAdapter { if (resource !== undefined) { const wsFolder = vscode.workspace.getWorkspaceFolder(resource); if (wsFolder !== undefined) { - query.searchLocations = [wsFolder.uri]; + query.searchLocations = { roots: [wsFolder.uri] }; } } diff --git a/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts b/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts index 665250b54f5c..ac531b7acbee 100644 --- a/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts +++ b/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts @@ -73,8 +73,7 @@ suite('Python envs locator utils - getQueryFilter', () => { const queries: PythonLocatorQuery[] = [ {}, { kinds: [] }, - { searchLocations: [] }, - { kinds: [], searchLocations: [] }, + // Any "defined" value for searchLocations causes filtering... ]; queries.forEach((query) => { test(`all envs kept (query ${query})`, () => { @@ -144,7 +143,11 @@ suite('Python envs locator utils - getQueryFilter', () => { suite('searchLocations', () => { test('match none', () => { - const query: PythonLocatorQuery = { searchLocations: [doesNotExist] }; + const query: PythonLocatorQuery = { + searchLocations: { + roots: [doesNotExist], + }, + }; const filter = getQueryFilter(query); const filtered = envs.filter(filter); @@ -154,11 +157,13 @@ suite('Python envs locator utils - getQueryFilter', () => { test('match one (multiple locations)', () => { const expected = [envSL4]; - const searchLocations = [ - envSL4.searchLocation!, - doesNotExist, - envSL4.searchLocation!, // repeated - ]; + const searchLocations = { + roots: [ + envSL4.searchLocation!, + doesNotExist, + envSL4.searchLocation!, // repeated + ], + }; const query: PythonLocatorQuery = { searchLocations }; const filter = getQueryFilter(query); @@ -169,9 +174,11 @@ suite('Python envs locator utils - getQueryFilter', () => { test('match multiple (one location)', () => { const expected = [envS3, envSL2]; - const searchLocations = [ - workspaceRoot, - ]; + const searchLocations = { + roots: [ + workspaceRoot, + ], + }; const query: PythonLocatorQuery = { searchLocations }; const filter = getQueryFilter(query); @@ -182,8 +189,10 @@ suite('Python envs locator utils - getQueryFilter', () => { test('match multiple (multiple locations)', () => { const expected = [envS3, ...rootedLocatedEnvs]; - const searchLocations = rootedLocatedEnvs.map((env) => env.searchLocation!); - searchLocations.push(doesNotExist); + const searchLocations = { + roots: rootedLocatedEnvs.map((env) => env.searchLocation!), + }; + searchLocations.roots.push(doesNotExist); const query: PythonLocatorQuery = { searchLocations }; const filter = getQueryFilter(query); @@ -194,8 +203,11 @@ suite('Python envs locator utils - getQueryFilter', () => { test('match multiple (include non-searched envs)', () => { const expected = [...plainEnvs, ...locatedEnvs, envS3, ...rootedLocatedEnvs]; - const searchLocations = [null, ...rootedLocatedEnvs.map((env) => env.searchLocation!)]; - searchLocations.push(doesNotExist); + const searchLocations = { + roots: rootedLocatedEnvs.map((env) => env.searchLocation!), + includeNonRooted: true, + }; + searchLocations.roots.push(doesNotExist); const query: PythonLocatorQuery = { searchLocations }; const filter = getQueryFilter(query); @@ -206,7 +218,9 @@ suite('Python envs locator utils - getQueryFilter', () => { test('match all searched', () => { const expected = [...rootedEnvs, ...rootedLocatedEnvs]; - const searchLocations = expected.map((env) => env.searchLocation!); + const searchLocations = { + roots: expected.map((env) => env.searchLocation!), + }; const query: PythonLocatorQuery = { searchLocations }; const filter = getQueryFilter(query); @@ -217,7 +231,10 @@ suite('Python envs locator utils - getQueryFilter', () => { test('match all (including non-searched)', () => { const expected = envs; - const searchLocations = [null, ...expected.map((env) => env.searchLocation!)]; + const searchLocations = { + roots: expected.map((env) => env.searchLocation!), + includeNonRooted: true, + }; const query: PythonLocatorQuery = { searchLocations }; const filter = getQueryFilter(query); @@ -228,7 +245,9 @@ suite('Python envs locator utils - getQueryFilter', () => { test('match all searched under one root', () => { const expected = [envS1, envS2, envSL1, envSL3, envSL5]; - const searchLocations = [Uri.file(homeDir)]; + const searchLocations = { + roots: [Uri.file(homeDir)], + }; const query: PythonLocatorQuery = { searchLocations }; const filter = getQueryFilter(query); @@ -237,9 +256,12 @@ suite('Python envs locator utils - getQueryFilter', () => { assert.deepEqual(filtered, expected); }); - test('match only non-searched envs (null location)', () => { + test('match only non-searched envs (empty roots)', () => { const expected = [...plainEnvs, ...locatedEnvs]; - const searchLocations = [null]; + const searchLocations = { + roots: [], + includeNonRooted: true, + }; const query: PythonLocatorQuery = { searchLocations }; const filter = getQueryFilter(query); @@ -250,8 +272,10 @@ suite('Python envs locator utils - getQueryFilter', () => { test('match only non-searched envs (with unmatched location)', () => { const expected = [...plainEnvs, ...locatedEnvs]; - const searchLocations: (Uri | null)[] = [null]; - searchLocations.push(doesNotExist); + const searchLocations = { + roots: [doesNotExist], + includeNonRooted: true, + }; const query: PythonLocatorQuery = { searchLocations }; const filter = getQueryFilter(query); @@ -259,23 +283,15 @@ suite('Python envs locator utils - getQueryFilter', () => { assert.deepEqual(filtered, expected); }); - - test('match only non-searched envs (explicit null)', () => { - const expected = [...plainEnvs, ...locatedEnvs]; - const query: PythonLocatorQuery = { searchLocations: null }; - - const filter = getQueryFilter(query); - const filtered = envs.filter(filter); - - assert.deepEqual(filtered, expected); - }); }); suite('mixed query', () => { test('match none', () => { const query: PythonLocatorQuery = { kinds: [PythonEnvKind.OtherGlobal], - searchLocations: [doesNotExist], + searchLocations: { + roots: [doesNotExist], + }, }; const filter = getQueryFilter(query); @@ -287,8 +303,10 @@ suite('Python envs locator utils - getQueryFilter', () => { test('match some', () => { const expected = [envSL1, envSL4, envSL5]; const kinds = [PythonEnvKind.Venv, PythonEnvKind.Custom]; - const searchLocations = rootedLocatedEnvs.map((env) => env.searchLocation!); - searchLocations.push(doesNotExist); + const searchLocations = { + roots: rootedLocatedEnvs.map((env) => env.searchLocation!), + }; + searchLocations.roots.push(doesNotExist); const query: PythonLocatorQuery = { kinds, searchLocations }; const filter = getQueryFilter(query); @@ -300,7 +318,9 @@ suite('Python envs locator utils - getQueryFilter', () => { test('match all', () => { const expected = [...rootedEnvs, ...rootedLocatedEnvs]; const kinds: PythonEnvKind[] = getEnumValues(PythonEnvKind); - const searchLocations = expected.map((env) => env.searchLocation!); + const searchLocations = { + roots: expected.map((env) => env.searchLocation!), + }; const query: PythonLocatorQuery = { kinds, searchLocations }; const filter = getQueryFilter(query); diff --git a/src/test/pythonEnvironments/base/locators.unit.test.ts b/src/test/pythonEnvironments/base/locators.unit.test.ts index 7621a9610a3d..4c76dfdff9e0 100644 --- a/src/test/pythonEnvironments/base/locators.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators.unit.test.ts @@ -96,7 +96,7 @@ suite('Python envs locators - Locators', () => { test('with query', async () => { const expected: PythonLocatorQuery = { kinds: [PythonEnvKind.Venv], - searchLocations: [Uri.file('???')] + searchLocations: { roots: [Uri.file('???')] }, }; let query: PythonLocatorQuery | undefined; async function onQuery(q: PythonLocatorQuery | undefined, e: PythonEnvInfo[]) { diff --git a/src/test/pythonEnvironments/discovery/locators/index.unit.test.ts b/src/test/pythonEnvironments/discovery/locators/index.unit.test.ts index e99982856a02..5dfed7117978 100644 --- a/src/test/pythonEnvironments/discovery/locators/index.unit.test.ts +++ b/src/test/pythonEnvironments/discovery/locators/index.unit.test.ts @@ -392,8 +392,9 @@ suite('WorkspaceLocators', () => { ]); const folders = new WorkspaceFolders([root1, root2]); locators.activate(folders); + const query = { searchLocations: { roots: [root1] } }; - const iterators = locators.iterEnvs({ searchLocations: [root1] }); + const iterators = locators.iterEnvs(query); const envs = await getEnvs(iterators); expect(envs).to.deep.equal(expected); @@ -417,8 +418,9 @@ suite('WorkspaceLocators', () => { ]); const folders = new WorkspaceFolders([root1, root2]); locators.activate(folders); + const query = { searchLocations: { roots: [root1, root2] } }; - const iterators = locators.iterEnvs({ searchLocations: [root1, root2] }); + const iterators = locators.iterEnvs(query); const envs = await getEnvs(iterators); expect(envs).to.deep.equal(expected); @@ -441,8 +443,9 @@ suite('WorkspaceLocators', () => { ]); const folders = new WorkspaceFolders([root1, root2]); locators.activate(folders); + const query = { searchLocations: { roots: [Uri.file('baz')] } }; - const iterators = locators.iterEnvs({ searchLocations: [Uri.file('baz')] }); + const iterators = locators.iterEnvs(query); const envs = await getEnvs(iterators); expect(envs).to.deep.equal([]); From 3d85fa70bb54c89c1e941d3d3beb4e9f4373a663 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Mon, 28 Sep 2020 13:36:56 -0600 Subject: [PATCH 21/25] Fix a sonarcloud warning. --- src/client/pythonEnvironments/base/locatorUtils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/client/pythonEnvironments/base/locatorUtils.ts b/src/client/pythonEnvironments/base/locatorUtils.ts index 36528b42fb72..c35a61cf28f0 100644 --- a/src/client/pythonEnvironments/base/locatorUtils.ts +++ b/src/client/pythonEnvironments/base/locatorUtils.ts @@ -44,7 +44,8 @@ export function getQueryFilter(query: PythonLocatorQuery): (env: PythonEnvInfo) return query.searchLocations === undefined; } // Check against the requested roots. - return locationFilters.some((filter) => filter(env.searchLocation!)); + const loc = env.searchLocation; + return locationFilters.some((filter) => filter(loc)); } } return (env) => { From e47322b6123171b5e338ac7eff9202ee6016768b Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Mon, 28 Sep 2020 14:02:17 -0600 Subject: [PATCH 22/25] Filter out undefined locations in the tests. --- src/client/pythonEnvironments/base/locatorUtils.ts | 5 ++--- src/test/pythonEnvironments/base/locatorUtils.unit.test.ts | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/client/pythonEnvironments/base/locatorUtils.ts b/src/client/pythonEnvironments/base/locatorUtils.ts index c35a61cf28f0..c6661e5d0e70 100644 --- a/src/client/pythonEnvironments/base/locatorUtils.ts +++ b/src/client/pythonEnvironments/base/locatorUtils.ts @@ -63,11 +63,10 @@ function getSearchLocationFilters(query: PythonLocatorQuery): ((u: Uri) => boole if (query.searchLocations === undefined) { return undefined; } - const candidates: Uri[] = query.searchLocations.roots.filter((u) => !!u); - if (candidates.length === 0) { + if (query.searchLocations.roots.length === 0) { return undefined; } - return candidates.map((loc) => getURIFilter(loc, { + return query.searchLocations.roots.map((loc) => getURIFilter(loc, { checkParent: true, checkExact: true, })); diff --git a/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts b/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts index ac531b7acbee..fe297360e7a8 100644 --- a/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts +++ b/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts @@ -232,7 +232,7 @@ suite('Python envs locator utils - getQueryFilter', () => { test('match all (including non-searched)', () => { const expected = envs; const searchLocations = { - roots: expected.map((env) => env.searchLocation!), + roots: expected.map((e) => e.searchLocation!).filter((e) => !!e), includeNonRooted: true, }; const query: PythonLocatorQuery = { searchLocations }; From f97bb3100d0ac597c88809d32d85c07bdbbbf7c4 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Mon, 28 Sep 2020 14:42:01 -0600 Subject: [PATCH 23/25] Simplify the searchLocations logic a little. --- src/client/pythonEnvironments/base/locatorUtils.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/client/pythonEnvironments/base/locatorUtils.ts b/src/client/pythonEnvironments/base/locatorUtils.ts index c6661e5d0e70..b7c2e496809f 100644 --- a/src/client/pythonEnvironments/base/locatorUtils.ts +++ b/src/client/pythonEnvironments/base/locatorUtils.ts @@ -40,12 +40,12 @@ export function getQueryFilter(query: PythonLocatorQuery): (env: PythonEnvInfo) return includeNonRooted; } else { // It is a "rooted" env. - if (locationFilters === undefined) { - return query.searchLocations === undefined; - } - // Check against the requested roots. const loc = env.searchLocation; - return locationFilters.some((filter) => filter(loc)); + if (locationFilters !== undefined) { + // Check against the requested roots. (There may be none.) + return locationFilters.some((filter) => filter(loc)); + } + return true; } } return (env) => { @@ -64,7 +64,7 @@ function getSearchLocationFilters(query: PythonLocatorQuery): ((u: Uri) => boole return undefined; } if (query.searchLocations.roots.length === 0) { - return undefined; + return []; } return query.searchLocations.roots.map((loc) => getURIFilter(loc, { checkParent: true, From 1edb76def8f5a5db71610658d0c6a88d5763c75b Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Mon, 28 Sep 2020 15:25:48 -0600 Subject: [PATCH 24/25] Strip trailing slashes in paths. --- src/client/common/utils/misc.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/client/common/utils/misc.ts b/src/client/common/utils/misc.ts index 74fd6efb4d4b..0a1186b6da0a 100644 --- a/src/client/common/utils/misc.ts +++ b/src/client/common/utils/misc.ts @@ -147,20 +147,28 @@ export function getURIFilter( checkExact?: boolean; } = { checkExact: true } ): (u: Uri) => boolean { - const uriRoot = uri.path.endsWith('/') ? uri.path : `${uri.path}/`; + let uriPath = uri.path; + while (uri.path.endsWith('/')) { + uriPath = uriPath.slice(0, -1); + } + const uriRoot = `${uriPath}/`; function filter(candidate: Uri): boolean { if (candidate.scheme !== uri.scheme) { return false; } - if (opts.checkExact && candidate.path === uri.path) { + let candidatePath = candidate.path; + while (candidate.path.endsWith('/')) { + candidatePath = candidatePath.slice(0, -1); + } + if (opts.checkExact && candidatePath === uriPath) { return true; } - if (opts.checkParent && candidate.path.startsWith(uriRoot)) { + if (opts.checkParent && candidatePath.startsWith(uriRoot)) { return true; } if (opts.checkChild) { - const candidateRoot = candidate.path.endsWith('/') ? candidate.path : '{candidate.path}/'; - if (uri.path.startsWith(candidateRoot)) { + const candidateRoot = `{candidatePath}/`; + if (uriPath.startsWith(candidateRoot)) { return true; } } From fd5e37bd68b77556919ff944fcf1dc19d102cd7e Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Mon, 28 Sep 2020 18:18:20 -0600 Subject: [PATCH 25/25] Add getMinimalPartialInfo(). --- .../pythonEnvironments/base/info/env.ts | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/src/client/pythonEnvironments/base/info/env.ts b/src/client/pythonEnvironments/base/info/env.ts index fdc556cbf5fe..bb72186ca7a5 100644 --- a/src/client/pythonEnvironments/base/info/env.ts +++ b/src/client/pythonEnvironments/base/info/env.ts @@ -97,6 +97,30 @@ function updateEnv(env: PythonEnvInfo, updates: { } } +/** + * For the given data, build a normalized partial info object. + * + * If insufficient data is provided to generate a minimal object, such + * that it is not identifiable, then `undefined` is returned. + */ +export function getMinimalPartialInfo(env: string | Partial): Partial | undefined { + if (typeof env === 'string') { + if (env === '') { + return undefined; + } + return { + executable: { filename: env, sysPrefix: '', ctime: -1, mtime: -1 }, + }; + } + if (env.executable === undefined) { + return undefined; + } + if (env.executable.filename === '') { + return undefined; + } + return env; +} + /** * Checks if two environments are same. * @param {string | PythonEnvInfo} left: environment to compare. @@ -110,13 +134,19 @@ function updateEnv(env: PythonEnvInfo, updates: { * where multiple versions of python executables are all put in the same directory. */ export function areSameEnv( - left: string | PythonEnvInfo, - right: string | PythonEnvInfo, + left: string | Partial, + right: string | Partial, allowPartialMatch?: boolean, -): boolean { - const leftFilename = typeof left === 'string' ? left : left.executable.filename; - const rightFilename = typeof right === 'string' ? right : right.executable.filename; +): boolean | undefined { + const leftInfo = getMinimalPartialInfo(left); + const rightInfo = getMinimalPartialInfo(right); + if (leftInfo === undefined || rightInfo === undefined) { + return undefined; + } + const leftFilename = leftInfo.executable!.filename; + const rightFilename = rightInfo.executable!.filename; + // For now we assume that matching executable means they are the same. if (arePathsSame(leftFilename, rightFilename)) { return true; }