From d6ba0a7779342c5872564b4b524e8c934f7518b0 Mon Sep 17 00:00:00 2001 From: mitchell Date: Thu, 19 Jan 2023 15:18:52 -0500 Subject: [PATCH 01/20] Detect ActiveState Python runtimes (#20532) --- .../configuration/environmentTypeComparer.ts | 1 + .../commands/setInterpreter.ts | 1 + .../pythonEnvironments/base/info/envKind.ts | 2 + .../pythonEnvironments/base/info/index.ts | 3 ++ .../base/locators/composite/resolverUtils.ts | 25 +++++++++ .../locators/lowLevel/activestateLocator.ts | 39 ++++++++++++++ .../common/environmentIdentifier.ts | 2 + .../common/environmentManagers/activestate.ts | 54 +++++++++++++++++++ src/client/pythonEnvironments/index.ts | 2 + src/client/pythonEnvironments/info/index.ts | 6 +++ src/client/pythonEnvironments/legacyIOC.ts | 1 + .../base/info/envKind.unit.test.ts | 1 + .../lowLevel/activestateLocator.unit.test.ts | 51 ++++++++++++++++++ .../activestate.unit.test.ts | 53 ++++++++++++++++++ .../2af6390a/_runtime_store/completed | 0 .../activestate/2af6390a/exec/not-python3 | 0 .../activestate/2af6390a/exec/not-python3.exe | 0 .../activestate/b6a0705d/exec/python3 | 1 + .../activestate/b6a0705d/exec/python3.exe | 1 + .../c09080d1/_runtime_store/completed | 0 .../activestate/c09080d1/exec/python3 | 0 .../activestate/c09080d1/exec/python3.exe | 0 22 files changed, 243 insertions(+) create mode 100644 src/client/pythonEnvironments/base/locators/lowLevel/activestateLocator.ts create mode 100644 src/client/pythonEnvironments/common/environmentManagers/activestate.ts create mode 100644 src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts create mode 100644 src/test/pythonEnvironments/common/environmentManagers/activestate.unit.test.ts create mode 100644 src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/_runtime_store/completed create mode 100644 src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/exec/not-python3 create mode 100644 src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/exec/not-python3.exe create mode 100644 src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3 create mode 100644 src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3.exe create mode 100644 src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/_runtime_store/completed create mode 100644 src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/exec/python3 create mode 100644 src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/exec/python3.exe diff --git a/src/client/interpreter/configuration/environmentTypeComparer.ts b/src/client/interpreter/configuration/environmentTypeComparer.ts index e3c77a6b2d6d..641be5c1ace7 100644 --- a/src/client/interpreter/configuration/environmentTypeComparer.ts +++ b/src/client/interpreter/configuration/environmentTypeComparer.ts @@ -234,6 +234,7 @@ function getPrioritizedEnvironmentType(): EnvironmentType[] { EnvironmentType.VirtualEnvWrapper, EnvironmentType.Venv, EnvironmentType.VirtualEnv, + EnvironmentType.ActiveState, EnvironmentType.Conda, EnvironmentType.Pyenv, EnvironmentType.MicrosoftStore, diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts index 39965adec0a0..1aee36a57cb7 100644 --- a/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts +++ b/src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts @@ -65,6 +65,7 @@ export namespace EnvGroups { export const Venv = 'Venv'; export const Poetry = 'Poetry'; export const VirtualEnvWrapper = 'VirtualEnvWrapper'; + export const ActiveState = 'ActiveState'; export const Recommended = Common.recommended; } diff --git a/src/client/pythonEnvironments/base/info/envKind.ts b/src/client/pythonEnvironments/base/info/envKind.ts index 09490725e960..8828003c5ce7 100644 --- a/src/client/pythonEnvironments/base/info/envKind.ts +++ b/src/client/pythonEnvironments/base/info/envKind.ts @@ -22,6 +22,7 @@ export function getKindDisplayName(kind: PythonEnvKind): string { [PythonEnvKind.VirtualEnvWrapper, 'virtualenv'], [PythonEnvKind.Pipenv, 'pipenv'], [PythonEnvKind.Conda, 'conda'], + [PythonEnvKind.ActiveState, 'ActiveState'], // For now we treat OtherVirtual like Unknown. ] as [PythonEnvKind, string][]) { if (kind === candidate) { @@ -63,6 +64,7 @@ export function getPrioritizedEnvKinds(): PythonEnvKind[] { PythonEnvKind.Venv, PythonEnvKind.VirtualEnvWrapper, PythonEnvKind.VirtualEnv, + PythonEnvKind.ActiveState, PythonEnvKind.OtherVirtual, PythonEnvKind.OtherGlobal, PythonEnvKind.System, diff --git a/src/client/pythonEnvironments/base/info/index.ts b/src/client/pythonEnvironments/base/info/index.ts index 4ef512c56ed6..a1c61d8067a3 100644 --- a/src/client/pythonEnvironments/base/info/index.ts +++ b/src/client/pythonEnvironments/base/info/index.ts @@ -15,6 +15,7 @@ export enum PythonEnvKind { MicrosoftStore = 'global-microsoft-store', Pyenv = 'global-pyenv', Poetry = 'poetry', + ActiveState = 'activestate', Custom = 'global-custom', OtherGlobal = 'global-other', // "virtual" @@ -28,6 +29,7 @@ export enum PythonEnvKind { export enum PythonEnvType { Conda = 'Conda', + ActiveState = 'ActiveState', Virtual = 'Virtual', } @@ -48,6 +50,7 @@ export const virtualEnvKinds = [ PythonEnvKind.VirtualEnvWrapper, PythonEnvKind.Conda, PythonEnvKind.VirtualEnv, + PythonEnvKind.ActiveState, ]; export const globallyInstalledEnvKinds = [ diff --git a/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts b/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts index c0506d4a06ba..d3dc65be2522 100644 --- a/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts +++ b/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts @@ -25,6 +25,7 @@ import { parseVersionFromExecutable } from '../../info/executable'; import { traceError, traceWarn } from '../../../../logging'; import { isVirtualEnvironment } from '../../../common/environmentManagers/simplevirtualenvs'; import { getWorkspaceFolderPaths } from '../../../../common/vscodeApis/workspaceApis'; +import { ActiveState, isActiveStateEnvironment } from '../../../common/environmentManagers/activestate'; function getResolvers(): Map Promise> { const resolvers = new Map Promise>(); @@ -37,6 +38,7 @@ function getResolvers(): Map Promise { return envInfo; } +async function resolveActiveStateEnv(env: BasicEnvInfo): Promise { + const info = buildEnvInfo({ + kind: env.kind, + executable: env.executablePath, + type: PythonEnvType.ActiveState, + }); + const projects = await ActiveState.getProjects(); + if (projects) { + for (const project of projects) { + for (const dir of project.executables) { + if (dir === path.dirname(env.executablePath)) { + info.name = `${project.organization}/${project.name}`; + return info; + } + } + } + } + return info; +} + async function isBaseCondaPyenvEnvironment(executablePath: string) { if (!(await isCondaEnvironment(executablePath))) { return false; diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/activestateLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/activestateLocator.ts new file mode 100644 index 000000000000..1b15ed1a20c7 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/activestateLocator.ts @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { ActiveState } from '../../../common/environmentManagers/activestate'; +import { PythonEnvKind } from '../../info'; +import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; +import { traceError, traceVerbose } from '../../../../logging'; +import { LazyResourceBasedLocator } from '../common/resourceBasedLocator'; +import { findInterpretersInDir } from '../../../common/commonUtils'; + +export class ActiveStateLocator extends LazyResourceBasedLocator { + public readonly providerId: string = 'activestate'; + + // eslint-disable-next-line class-methods-use-this + public async *doIterEnvs(): IPythonEnvsIterator { + const projects = await ActiveState.getProjects(); + if (projects === undefined) { + traceVerbose(`Couldn't fetch State Tool projects.`); + return; + } + for (const project of projects) { + if (project.executables) { + for (const dir of project.executables) { + try { + traceVerbose(`Looking for Python in: ${project.name}`); + for await (const exe of findInterpretersInDir(dir)) { + traceVerbose(`Found Python executable: ${exe.filename}`); + yield { kind: PythonEnvKind.ActiveState, executablePath: exe.filename }; + } + } catch (ex) { + traceError(`Failed to process State Tool project: ${JSON.stringify(project)}`, ex); + } + } + } + } + } +} diff --git a/src/client/pythonEnvironments/common/environmentIdentifier.ts b/src/client/pythonEnvironments/common/environmentIdentifier.ts index 957321ed8e61..2dbc8b2b93d9 100644 --- a/src/client/pythonEnvironments/common/environmentIdentifier.ts +++ b/src/client/pythonEnvironments/common/environmentIdentifier.ts @@ -15,6 +15,7 @@ import { isVirtualenvwrapperEnvironment as isVirtualEnvWrapperEnvironment, } from './environmentManagers/simplevirtualenvs'; import { isMicrosoftStoreEnvironment } from './environmentManagers/microsoftStoreEnv'; +import { isActiveStateEnvironment } from './environmentManagers/activestate'; function getIdentifiers(): Map Promise> { const notImplemented = () => Promise.resolve(false); @@ -32,6 +33,7 @@ function getIdentifiers(): Map Promise identifier.set(PythonEnvKind.Venv, isVenvEnvironment); identifier.set(PythonEnvKind.VirtualEnvWrapper, isVirtualEnvWrapperEnvironment); identifier.set(PythonEnvKind.VirtualEnv, isVirtualEnvEnvironment); + identifier.set(PythonEnvKind.ActiveState, isActiveStateEnvironment); identifier.set(PythonEnvKind.Unknown, defaultTrue); identifier.set(PythonEnvKind.OtherGlobal, isGloballyInstalledEnv); return identifier; diff --git a/src/client/pythonEnvironments/common/environmentManagers/activestate.ts b/src/client/pythonEnvironments/common/environmentManagers/activestate.ts new file mode 100644 index 000000000000..3e28a11d6d14 --- /dev/null +++ b/src/client/pythonEnvironments/common/environmentManagers/activestate.ts @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import { pathExists, shellExecute } from '../externalDependencies'; +import { cache } from '../../../common/utils/decorators'; +import { traceError, traceVerbose } from '../../../logging'; + +const STATE_GENERAL_TIMEOUT = 50000; + +export type ProjectInfo = { + name: string; + organization: string; + local_checkouts: string[]; // eslint-disable-line camelcase + executables: string[]; +}; + +export async function isActiveStateEnvironment(interpreterPath: string): Promise { + const execDir = path.dirname(interpreterPath); + const runtimeDir = path.dirname(execDir); + return pathExists(path.join(runtimeDir, '_runtime_store')); +} + +export class ActiveState { + public static readonly stateCommand: string = 'state'; + + public static async getProjects(): Promise { + return this.getProjectsCached(); + } + + @cache(30_000, true, 10_000) + private static async getProjectsCached(): Promise { + try { + const result = await shellExecute(`${this.stateCommand} projects -o editor`, { + timeout: STATE_GENERAL_TIMEOUT, + }); + if (!result) { + return undefined; + } + let output = result.stdout.trimEnd(); + if (output[output.length - 1] === '\0') { + // '\0' is a record separator. + output = output.substring(0, output.length - 1); + } + traceVerbose(`${this.stateCommand} projects -o editor: ${output}`); + return JSON.parse(output); + } catch (ex) { + traceError(ex); + return undefined; + } + } +} diff --git a/src/client/pythonEnvironments/index.ts b/src/client/pythonEnvironments/index.ts index 3f7bac7d670e..8d6a8cb7d4ba 100644 --- a/src/client/pythonEnvironments/index.ts +++ b/src/client/pythonEnvironments/index.ts @@ -36,6 +36,7 @@ import { import { EnvsCollectionService } from './base/locators/composite/envsCollectionService'; import { IDisposable } from '../common/types'; import { traceError } from '../logging'; +import { ActiveStateLocator } from './base/locators/lowLevel/activestateLocator'; /** * Set up the Python environments component (during extension activation).' @@ -137,6 +138,7 @@ function createNonWorkspaceLocators(ext: ExtensionState): ILocator // OS-independent locators go here. new PyenvLocator(), new CondaEnvironmentLocator(), + new ActiveStateLocator(), new GlobalVirtualEnvironmentLocator(), new CustomVirtualEnvironmentLocator(), ); diff --git a/src/client/pythonEnvironments/info/index.ts b/src/client/pythonEnvironments/info/index.ts index d0f41c45a5b1..1ee8b46202f7 100644 --- a/src/client/pythonEnvironments/info/index.ts +++ b/src/client/pythonEnvironments/info/index.ts @@ -19,6 +19,7 @@ export enum EnvironmentType { MicrosoftStore = 'MicrosoftStore', Poetry = 'Poetry', VirtualEnvWrapper = 'VirtualEnvWrapper', + ActiveState = 'ActiveState', Global = 'Global', System = 'System', } @@ -30,6 +31,7 @@ export const virtualEnvTypes = [ EnvironmentType.VirtualEnvWrapper, EnvironmentType.Conda, EnvironmentType.VirtualEnv, + EnvironmentType.ActiveState, ]; /** @@ -41,6 +43,7 @@ export enum ModuleInstallerType { Pip = 'Pip', Poetry = 'Poetry', Pipenv = 'Pipenv', + ActiveState = 'ActiveState', } /** @@ -114,6 +117,9 @@ export function getEnvironmentTypeName(environmentType: EnvironmentType): string case EnvironmentType.VirtualEnvWrapper: { return 'virtualenvwrapper'; } + case EnvironmentType.ActiveState: { + return 'activestate'; + } default: { return ''; } diff --git a/src/client/pythonEnvironments/legacyIOC.ts b/src/client/pythonEnvironments/legacyIOC.ts index a3b8c3f0aaf7..ce9bfb4caf11 100644 --- a/src/client/pythonEnvironments/legacyIOC.ts +++ b/src/client/pythonEnvironments/legacyIOC.ts @@ -35,6 +35,7 @@ const convertedKinds = new Map( [PythonEnvKind.Poetry]: EnvironmentType.Poetry, [PythonEnvKind.Venv]: EnvironmentType.Venv, [PythonEnvKind.VirtualEnvWrapper]: EnvironmentType.VirtualEnvWrapper, + [PythonEnvKind.ActiveState]: EnvironmentType.ActiveState, }), ); diff --git a/src/test/pythonEnvironments/base/info/envKind.unit.test.ts b/src/test/pythonEnvironments/base/info/envKind.unit.test.ts index c12777d3e653..fdf174b4c551 100644 --- a/src/test/pythonEnvironments/base/info/envKind.unit.test.ts +++ b/src/test/pythonEnvironments/base/info/envKind.unit.test.ts @@ -20,6 +20,7 @@ const KIND_NAMES: [PythonEnvKind, string][] = [ [PythonEnvKind.VirtualEnvWrapper, 'virtualenvWrapper'], [PythonEnvKind.Pipenv, 'pipenv'], [PythonEnvKind.Conda, 'conda'], + [PythonEnvKind.ActiveState, 'activestate'], [PythonEnvKind.OtherVirtual, 'otherVirtual'], ]; diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts new file mode 100644 index 000000000000..c534050f01e9 --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as sinon from 'sinon'; +import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; +import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; +import { ActiveStateLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/activestateLocator'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { assertBasicEnvsEqual } from '../envTestUtils'; +import { ExecutionResult } from '../../../../../client/common/process/types'; +import { createBasicEnv } from '../../common'; +import { getOSType, OSType } from '../../../../../client/common/utils/platform'; + +suite('ActiveState Locator', () => { + const testActiveStateDir = path.join(TEST_LAYOUT_ROOT, 'activestate'); + let shellExecute: sinon.SinonStub; + let locator: ActiveStateLocator; + + suiteSetup(() => { + locator = new ActiveStateLocator(); + shellExecute = sinon.stub(externalDependencies, 'shellExecute'); + shellExecute.callsFake((command: string) => { + if (command === 'state projects -o editor') { + return Promise.resolve>({ + stdout: `[{"name":"test","organization":"test-org","local_checkouts":["does-not-matter"],"executables":["${testActiveStateDir}/c09080d1/exec"]},{"name":"test2","organization":"test-org","local_checkouts":["does-not-matter2"],"executables":["${testActiveStateDir}/2af6390a/exec"]}]\n\0`, + }); + } + return Promise.reject(new Error('Command failed')); + }); + }); + + suiteTeardown(() => sinon.restore()); + + test('iterEnvs()', async () => { + const actualEnvs = await getEnvs(locator.iterEnvs()); + const expectedEnvs = [ + createBasicEnv( + PythonEnvKind.ActiveState, + path.join( + testActiveStateDir, + 'c09080d1', + 'exec', + getOSType() === OSType.Windows ? 'python3.exe' : 'python3', + ), + ), + ]; + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); +}); diff --git a/src/test/pythonEnvironments/common/environmentManagers/activestate.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/activestate.unit.test.ts new file mode 100644 index 000000000000..8e86e7806ce5 --- /dev/null +++ b/src/test/pythonEnvironments/common/environmentManagers/activestate.unit.test.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { IFileSystem } from '../../../../client/common/platform/types'; +import { getOSType, OSType } from '../../../../client/common/utils/platform'; +import { isActiveStateEnvironment } from '../../../../client/pythonEnvironments/common/environmentManagers/activestate'; +import { TEST_LAYOUT_ROOT } from '../commonTestConstants'; + +suite('isActiveStateEnvironment Tests', () => { + const testActiveStateDir = path.join(TEST_LAYOUT_ROOT, 'activestate'); + let fileSystem: TypeMoq.IMock; + + setup(() => { + fileSystem = TypeMoq.Mock.ofType(); + }); + + test('Return true if runtime is set up', async () => { + const runtimeStorePath = path.join(testActiveStateDir, 'c09080d1', '_runtime_store'); + fileSystem + .setup((f) => f.directoryExists(TypeMoq.It.isValue(runtimeStorePath))) + .returns(() => Promise.resolve(true)); + + const result = await isActiveStateEnvironment( + path.join( + testActiveStateDir, + 'c09080d1', + 'exec', + getOSType() === OSType.Windows ? 'python3.exe' : 'python3', + ), + ); + expect(result).to.equal(true); + }); + + test(`Return false if the runtime is not set up`, async () => { + const runtimeStorePath = path.join(testActiveStateDir, 'b6a0705d', '_runtime_store'); + fileSystem + .setup((f) => f.directoryExists(TypeMoq.It.isValue(runtimeStorePath))) + .returns(() => Promise.resolve(false)); + + const result = await isActiveStateEnvironment( + path.join( + testActiveStateDir, + 'b6a0705d', + 'exec', + getOSType() === OSType.Windows ? 'python3.exe' : 'python3', + ), + ); + expect(result).to.equal(false); + }); +}); diff --git a/src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/_runtime_store/completed b/src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/_runtime_store/completed new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/exec/not-python3 b/src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/exec/not-python3 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/exec/not-python3.exe b/src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/exec/not-python3.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3 b/src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3 new file mode 100644 index 000000000000..0800f9b4dfd2 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3 @@ -0,0 +1 @@ +invalid python interpreter: missing _runtime_store diff --git a/src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3.exe b/src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3.exe new file mode 100644 index 000000000000..0800f9b4dfd2 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3.exe @@ -0,0 +1 @@ +invalid python interpreter: missing _runtime_store diff --git a/src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/_runtime_store/completed b/src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/_runtime_store/completed new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/exec/python3 b/src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/exec/python3 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/exec/python3.exe b/src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/exec/python3.exe new file mode 100644 index 000000000000..e69de29bb2d1 From 3381fe5811b52041b1322e5eb7650303182e32cd Mon Sep 17 00:00:00 2001 From: mitchell Date: Mon, 23 Jan 2023 13:23:54 -0500 Subject: [PATCH 02/20] Attempt to fix Windows test failure. --- .../locators/lowLevel/activestateLocator.unit.test.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts index c534050f01e9..0923827fd9f4 100644 --- a/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts @@ -24,7 +24,15 @@ suite('ActiveState Locator', () => { shellExecute.callsFake((command: string) => { if (command === 'state projects -o editor') { return Promise.resolve>({ - stdout: `[{"name":"test","organization":"test-org","local_checkouts":["does-not-matter"],"executables":["${testActiveStateDir}/c09080d1/exec"]},{"name":"test2","organization":"test-org","local_checkouts":["does-not-matter2"],"executables":["${testActiveStateDir}/2af6390a/exec"]}]\n\0`, + stdout: `[{"name":"test","organization":"test-org","local_checkouts":["does-not-matter"],"executables":["${path.join( + testActiveStateDir, + 'c09080d1', + 'exec', + )}]},{"name":"test2","organization":"test-org","local_checkouts":["does-not-matter2"],"executables":["${path.join( + testActiveStateDir, + '2af6390a', + 'exec', + )}]}]\n\0`, }); } return Promise.reject(new Error('Command failed')); From b768ae6c0a63df831d503be7d8143919d4af4fd6 Mon Sep 17 00:00:00 2001 From: mitchell Date: Mon, 23 Jan 2023 16:06:04 -0500 Subject: [PATCH 03/20] Fixed invalid JSON in unit test. --- .../base/locators/lowLevel/activestateLocator.unit.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts index 0923827fd9f4..186184b1d07e 100644 --- a/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts @@ -28,11 +28,11 @@ suite('ActiveState Locator', () => { testActiveStateDir, 'c09080d1', 'exec', - )}]},{"name":"test2","organization":"test-org","local_checkouts":["does-not-matter2"],"executables":["${path.join( + )}"]},{"name":"test2","organization":"test-org","local_checkouts":["does-not-matter2"],"executables":["${path.join( testActiveStateDir, '2af6390a', 'exec', - )}]}]\n\0`, + )}"]}]\n\0`, }); } return Promise.reject(new Error('Command failed')); From 625dd7cfc2ae1f26ee1cbfad1d05dfb5cd41da34 Mon Sep 17 00:00:00 2001 From: mitchell Date: Mon, 23 Jan 2023 16:06:23 -0500 Subject: [PATCH 04/20] Reduce state command timeout. --- .../common/environmentManagers/activestate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/pythonEnvironments/common/environmentManagers/activestate.ts b/src/client/pythonEnvironments/common/environmentManagers/activestate.ts index 3e28a11d6d14..04aca0f77893 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/activestate.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/activestate.ts @@ -8,7 +8,7 @@ import { pathExists, shellExecute } from '../externalDependencies'; import { cache } from '../../../common/utils/decorators'; import { traceError, traceVerbose } from '../../../logging'; -const STATE_GENERAL_TIMEOUT = 50000; +const STATE_GENERAL_TIMEOUT = 5000; export type ProjectInfo = { name: string; From 951d5226906dad83c320205e78df5e90f87aecb1 Mon Sep 17 00:00:00 2001 From: mitchell Date: Mon, 23 Jan 2023 16:07:15 -0500 Subject: [PATCH 05/20] Address some PR feedback. --- .../base/locators/composite/resolverUtils.ts | 2 +- .../common/environmentManagers/activestate.unit.test.ts | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts b/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts index d3dc65be2522..1eadf1e1991f 100644 --- a/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts +++ b/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts @@ -251,7 +251,7 @@ async function resolveActiveStateEnv(env: BasicEnvInfo): Promise if (projects) { for (const project of projects) { for (const dir of project.executables) { - if (dir === path.dirname(env.executablePath)) { + if (arePathsSame(dir, path.dirname(env.executablePath))) { info.name = `${project.organization}/${project.name}`; return info; } diff --git a/src/test/pythonEnvironments/common/environmentManagers/activestate.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/activestate.unit.test.ts index 8e86e7806ce5..d30394465210 100644 --- a/src/test/pythonEnvironments/common/environmentManagers/activestate.unit.test.ts +++ b/src/test/pythonEnvironments/common/environmentManagers/activestate.unit.test.ts @@ -35,11 +35,6 @@ suite('isActiveStateEnvironment Tests', () => { }); test(`Return false if the runtime is not set up`, async () => { - const runtimeStorePath = path.join(testActiveStateDir, 'b6a0705d', '_runtime_store'); - fileSystem - .setup((f) => f.directoryExists(TypeMoq.It.isValue(runtimeStorePath))) - .returns(() => Promise.resolve(false)); - const result = await isActiveStateEnvironment( path.join( testActiveStateDir, From b62dd06ebbd462a4639c7e4ecdaa25a15f494843 Mon Sep 17 00:00:00 2001 From: mitchell Date: Mon, 23 Jan 2023 16:13:55 -0500 Subject: [PATCH 06/20] Improve ActiveState State Tool detection. Try to avoid running the `state` binary, as it's expensive. If its installation path does not exist, it is not installed. --- .../base/locators/composite/resolverUtils.ts | 2 +- .../locators/lowLevel/activestateLocator.ts | 7 +++- .../common/environmentManagers/activestate.ts | 40 ++++++++++++++++--- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts b/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts index 1eadf1e1991f..506e281533e4 100644 --- a/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts +++ b/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts @@ -247,7 +247,7 @@ async function resolveActiveStateEnv(env: BasicEnvInfo): Promise executable: env.executablePath, type: PythonEnvType.ActiveState, }); - const projects = await ActiveState.getProjects(); + const projects = await ActiveState.getState().then((v) => v?.getProjects()); if (projects) { for (const project of projects) { for (const dir of project.executables) { diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/activestateLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/activestateLocator.ts index 1b15ed1a20c7..315f8e283d85 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/activestateLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/activestateLocator.ts @@ -15,7 +15,12 @@ export class ActiveStateLocator extends LazyResourceBasedLocator { // eslint-disable-next-line class-methods-use-this public async *doIterEnvs(): IPythonEnvsIterator { - const projects = await ActiveState.getProjects(); + const state = await ActiveState.getState(); + if (state === undefined) { + traceVerbose(`Couldn't locate the state binary.`); + return; + } + const projects = await state.getProjects(); if (projects === undefined) { traceVerbose(`Couldn't fetch State Tool projects.`); return; diff --git a/src/client/pythonEnvironments/common/environmentManagers/activestate.ts b/src/client/pythonEnvironments/common/environmentManagers/activestate.ts index 04aca0f77893..2fb9307344b2 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/activestate.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/activestate.ts @@ -7,6 +7,8 @@ import * as path from 'path'; import { pathExists, shellExecute } from '../externalDependencies'; import { cache } from '../../../common/utils/decorators'; import { traceError, traceVerbose } from '../../../logging'; +import { isTestExecution } from '../../../common/constants'; +import { getOSType, getUserHomeDir, OSType } from '../../../common/utils/platform'; const STATE_GENERAL_TIMEOUT = 5000; @@ -24,16 +26,44 @@ export async function isActiveStateEnvironment(interpreterPath: string): Promise } export class ActiveState { - public static readonly stateCommand: string = 'state'; + private static statePromise: Promise | undefined; - public static async getProjects(): Promise { + public static async getState(): Promise { + if (ActiveState.statePromise === undefined || isTestExecution()) { + ActiveState.statePromise = ActiveState.locate(); + } + return ActiveState.statePromise; + } + + public static getStateToolDir(): string | undefined { + const home = getUserHomeDir(); + if (!home) { + return undefined; + } + return getOSType() === OSType.Windows + ? path.join(home, 'AppData', 'Local', 'ActiveState', 'StateTool') + : path.join(home, '.local', 'ActiveState', 'StateTool'); + } + + private static async locate(): Promise { + const stateToolDir = this.getStateToolDir(); + if ((stateToolDir && (await pathExists(stateToolDir))) || isTestExecution()) { + return new ActiveState(); + } + return undefined; + } + + public async getProjects(): Promise { return this.getProjectsCached(); } + private static readonly stateCommand: string = 'state'; + @cache(30_000, true, 10_000) - private static async getProjectsCached(): Promise { + // eslint-disable-next-line class-methods-use-this + private async getProjectsCached(): Promise { try { - const result = await shellExecute(`${this.stateCommand} projects -o editor`, { + const result = await shellExecute(`${ActiveState.stateCommand} projects -o editor`, { timeout: STATE_GENERAL_TIMEOUT, }); if (!result) { @@ -44,7 +74,7 @@ export class ActiveState { // '\0' is a record separator. output = output.substring(0, output.length - 1); } - traceVerbose(`${this.stateCommand} projects -o editor: ${output}`); + traceVerbose(`${ActiveState.stateCommand} projects -o editor: ${output}`); return JSON.parse(output); } catch (ex) { traceError(ex); From 97a0b57fbb042f6358635df6e8b48fa012c5d8cd Mon Sep 17 00:00:00 2001 From: mitchell Date: Mon, 23 Jan 2023 16:35:17 -0500 Subject: [PATCH 07/20] Addressing more PR feedback. --- src/client/pythonEnvironments/base/info/index.ts | 1 - .../base/locators/composite/resolverUtils.ts | 6 +----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/client/pythonEnvironments/base/info/index.ts b/src/client/pythonEnvironments/base/info/index.ts index a1c61d8067a3..ddf0d2fbccc0 100644 --- a/src/client/pythonEnvironments/base/info/index.ts +++ b/src/client/pythonEnvironments/base/info/index.ts @@ -29,7 +29,6 @@ export enum PythonEnvKind { export enum PythonEnvType { Conda = 'Conda', - ActiveState = 'ActiveState', Virtual = 'Virtual', } diff --git a/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts b/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts index 506e281533e4..b047aee7ad5b 100644 --- a/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts +++ b/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts @@ -25,7 +25,7 @@ import { parseVersionFromExecutable } from '../../info/executable'; import { traceError, traceWarn } from '../../../../logging'; import { isVirtualEnvironment } from '../../../common/environmentManagers/simplevirtualenvs'; import { getWorkspaceFolderPaths } from '../../../../common/vscodeApis/workspaceApis'; -import { ActiveState, isActiveStateEnvironment } from '../../../common/environmentManagers/activestate'; +import { ActiveState } from '../../../common/environmentManagers/activestate'; function getResolvers(): Map Promise> { const resolvers = new Map Promise>(); @@ -80,9 +80,6 @@ async function getEnvType(env: PythonEnvInfo) { if (await isCondaEnvironment(env.executable.filename)) { return PythonEnvType.Conda; } - if (await isActiveStateEnvironment(env.executable.filename)) { - return PythonEnvType.ActiveState; - } return undefined; } @@ -245,7 +242,6 @@ async function resolveActiveStateEnv(env: BasicEnvInfo): Promise const info = buildEnvInfo({ kind: env.kind, executable: env.executablePath, - type: PythonEnvType.ActiveState, }); const projects = await ActiveState.getState().then((v) => v?.getProjects()); if (projects) { From 819d16fbe81d8cca742c0814f5f669b0c6bd9e10 Mon Sep 17 00:00:00 2001 From: mitchell Date: Tue, 24 Jan 2023 14:51:21 -0500 Subject: [PATCH 08/20] Remove unnecessary IFilesystem mocks. --- .../environmentManagers/activestate.unit.test.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/test/pythonEnvironments/common/environmentManagers/activestate.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/activestate.unit.test.ts index d30394465210..23eebc5fee07 100644 --- a/src/test/pythonEnvironments/common/environmentManagers/activestate.unit.test.ts +++ b/src/test/pythonEnvironments/common/environmentManagers/activestate.unit.test.ts @@ -3,26 +3,14 @@ import { expect } from 'chai'; import * as path from 'path'; -import * as TypeMoq from 'typemoq'; -import { IFileSystem } from '../../../../client/common/platform/types'; import { getOSType, OSType } from '../../../../client/common/utils/platform'; import { isActiveStateEnvironment } from '../../../../client/pythonEnvironments/common/environmentManagers/activestate'; import { TEST_LAYOUT_ROOT } from '../commonTestConstants'; suite('isActiveStateEnvironment Tests', () => { const testActiveStateDir = path.join(TEST_LAYOUT_ROOT, 'activestate'); - let fileSystem: TypeMoq.IMock; - - setup(() => { - fileSystem = TypeMoq.Mock.ofType(); - }); test('Return true if runtime is set up', async () => { - const runtimeStorePath = path.join(testActiveStateDir, 'c09080d1', '_runtime_store'); - fileSystem - .setup((f) => f.directoryExists(TypeMoq.It.isValue(runtimeStorePath))) - .returns(() => Promise.resolve(true)); - const result = await isActiveStateEnvironment( path.join( testActiveStateDir, From 1850c1377e9e7f7fbddf698ef68effb8bced027a Mon Sep 17 00:00:00 2001 From: mitchell Date: Tue, 24 Jan 2023 14:51:39 -0500 Subject: [PATCH 09/20] Mock up an ActiveState State Tool installation in tests. --- .../common/environmentManagers/activestate.ts | 5 ++- .../lowLevel/activestateLocator.unit.test.ts | 33 +++++++++++++++---- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/client/pythonEnvironments/common/environmentManagers/activestate.ts b/src/client/pythonEnvironments/common/environmentManagers/activestate.ts index 2fb9307344b2..1adde4c7df6d 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/activestate.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/activestate.ts @@ -7,7 +7,6 @@ import * as path from 'path'; import { pathExists, shellExecute } from '../externalDependencies'; import { cache } from '../../../common/utils/decorators'; import { traceError, traceVerbose } from '../../../logging'; -import { isTestExecution } from '../../../common/constants'; import { getOSType, getUserHomeDir, OSType } from '../../../common/utils/platform'; const STATE_GENERAL_TIMEOUT = 5000; @@ -29,7 +28,7 @@ export class ActiveState { private static statePromise: Promise | undefined; public static async getState(): Promise { - if (ActiveState.statePromise === undefined || isTestExecution()) { + if (ActiveState.statePromise === undefined) { ActiveState.statePromise = ActiveState.locate(); } return ActiveState.statePromise; @@ -47,7 +46,7 @@ export class ActiveState { private static async locate(): Promise { const stateToolDir = this.getStateToolDir(); - if ((stateToolDir && (await pathExists(stateToolDir))) || isTestExecution()) { + if (stateToolDir && (await pathExists(stateToolDir))) { return new ActiveState(); } return undefined; diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts index 186184b1d07e..cef458fe3c04 100644 --- a/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts @@ -3,6 +3,7 @@ import * as path from 'path'; import * as sinon from 'sinon'; +import * as fsapi from 'fs-extra'; import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; @@ -11,17 +12,35 @@ import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; import { assertBasicEnvsEqual } from '../envTestUtils'; import { ExecutionResult } from '../../../../../client/common/process/types'; import { createBasicEnv } from '../../common'; -import { getOSType, OSType } from '../../../../../client/common/utils/platform'; +import * as platform from '../../../../../client/common/utils/platform'; +import { ActiveState } from '../../../../../client/pythonEnvironments/common/environmentManagers/activestate'; suite('ActiveState Locator', () => { const testActiveStateDir = path.join(TEST_LAYOUT_ROOT, 'activestate'); - let shellExecute: sinon.SinonStub; let locator: ActiveStateLocator; - suiteSetup(() => { + setup(() => { locator = new ActiveStateLocator(); - shellExecute = sinon.stub(externalDependencies, 'shellExecute'); - shellExecute.callsFake((command: string) => { + + let homeDir: string; + switch (platform.getOSType()) { + case platform.OSType.Windows: + homeDir = 'C:\\Users\\user'; + break; + case platform.OSType.OSX: + homeDir = '/Users/user'; + break; + default: + homeDir = '/home/user'; + } + sinon.stub(platform, 'getUserHomeDir').returns(homeDir); + + const stateToolDir = ActiveState.getStateToolDir(); + if (stateToolDir) { + sinon.stub(fsapi, 'pathExists').callsFake((dir: string) => dir === stateToolDir); + } + + sinon.stub(externalDependencies, 'shellExecute').callsFake((command: string) => { if (command === 'state projects -o editor') { return Promise.resolve>({ stdout: `[{"name":"test","organization":"test-org","local_checkouts":["does-not-matter"],"executables":["${path.join( @@ -39,7 +58,7 @@ suite('ActiveState Locator', () => { }); }); - suiteTeardown(() => sinon.restore()); + teardown(() => sinon.restore()); test('iterEnvs()', async () => { const actualEnvs = await getEnvs(locator.iterEnvs()); @@ -50,7 +69,7 @@ suite('ActiveState Locator', () => { testActiveStateDir, 'c09080d1', 'exec', - getOSType() === OSType.Windows ? 'python3.exe' : 'python3', + platform.getOSType() === platform.OSType.Windows ? 'python3.exe' : 'python3', ), ), ]; From 29633fa0547e55103e4928a8bc28bfb80d98832c Mon Sep 17 00:00:00 2001 From: mitchell Date: Wed, 25 Jan 2023 11:17:43 -0500 Subject: [PATCH 10/20] Recommend ActiveState Python runtimes for the workspaces they are associated with. --- .../configuration/environmentTypeComparer.ts | 4 +++ .../locators/lowLevel/activestateLocator.ts | 1 + .../common/environmentManagers/activestate.ts | 32 ++++++++++++++++++- 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/client/interpreter/configuration/environmentTypeComparer.ts b/src/client/interpreter/configuration/environmentTypeComparer.ts index 641be5c1ace7..808a364edf84 100644 --- a/src/client/interpreter/configuration/environmentTypeComparer.ts +++ b/src/client/interpreter/configuration/environmentTypeComparer.ts @@ -4,6 +4,7 @@ import { injectable, inject } from 'inversify'; import { Resource } from '../../common/types'; import { Architecture } from '../../common/utils/platform'; +import { isActiveStateEnvironmentForWorkspace } from '../../pythonEnvironments/common/environmentManagers/activestate'; import { isParentPath } from '../../pythonEnvironments/common/externalDependencies'; import { EnvironmentType, PythonEnvironment, virtualEnvTypes } from '../../pythonEnvironments/info'; import { PythonVersion } from '../../pythonEnvironments/info/pythonVersion'; @@ -93,6 +94,9 @@ export class EnvironmentTypeComparer implements IInterpreterComparer { if (isProblematicCondaEnvironment(i)) { return false; } + if (workspaceUri && isActiveStateEnvironmentForWorkspace(i.path, workspaceUri.folderUri.fsPath)) { + return true; + } if (getEnvLocationHeuristic(i, workspaceUri?.folderUri.fsPath || '') === EnvLocationHeuristic.Local) { return true; } diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/activestateLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/activestateLocator.ts index 315f8e283d85..e889d5eb2cd1 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/activestateLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/activestateLocator.ts @@ -25,6 +25,7 @@ export class ActiveStateLocator extends LazyResourceBasedLocator { traceVerbose(`Couldn't fetch State Tool projects.`); return; } + ActiveState.setCachedProjectInfo(projects); for (const project of projects) { if (project.executables) { for (const dir of project.executables) { diff --git a/src/client/pythonEnvironments/common/environmentManagers/activestate.ts b/src/client/pythonEnvironments/common/environmentManagers/activestate.ts index 1adde4c7df6d..d6eff48dd250 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/activestate.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/activestate.ts @@ -4,7 +4,8 @@ 'use strict'; import * as path from 'path'; -import { pathExists, shellExecute } from '../externalDependencies'; +import { dirname } from 'path'; +import { arePathsSame, pathExists, shellExecute } from '../externalDependencies'; import { cache } from '../../../common/utils/decorators'; import { traceError, traceVerbose } from '../../../logging'; import { getOSType, getUserHomeDir, OSType } from '../../../common/utils/platform'; @@ -80,4 +81,33 @@ export class ActiveState { return undefined; } } + + // Stored copy of known projects. isActiveStateEnvironmentForWorkspace() is + // not async, so getProjects() cannot be used. ActiveStateLocator sets this + // when it resolves project info. + private static cachedProjectInfo: ProjectInfo[] = []; + + public static getCachedProjectInfo(): ProjectInfo[] { + return this.cachedProjectInfo; + } + + public static setCachedProjectInfo(projects: ProjectInfo[]): void { + this.cachedProjectInfo = projects; + } +} + +export function isActiveStateEnvironmentForWorkspace(interpreterPath: string, workspacePath: string): boolean { + const interpreterDir = dirname(interpreterPath); + for (const project of ActiveState.getCachedProjectInfo()) { + if (project.executables) { + for (const [i, dir] of project.executables.entries()) { + // Note multiple checkouts for the same interpreter may exist. + // Check them all. + if (arePathsSame(dir, interpreterDir) && arePathsSame(workspacePath, project.local_checkouts[i])) { + return true; + } + } + } + } + return false; } From 1a0d3c4861feab5fd4ee45939bf889c64556c86c Mon Sep 17 00:00:00 2001 From: mitchell Date: Wed, 25 Jan 2023 15:37:11 -0500 Subject: [PATCH 11/20] Fixed failing unit test by checking for defined interpreter path. --- src/client/interpreter/configuration/environmentTypeComparer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/interpreter/configuration/environmentTypeComparer.ts b/src/client/interpreter/configuration/environmentTypeComparer.ts index 808a364edf84..d8431c9a8eb0 100644 --- a/src/client/interpreter/configuration/environmentTypeComparer.ts +++ b/src/client/interpreter/configuration/environmentTypeComparer.ts @@ -94,7 +94,7 @@ export class EnvironmentTypeComparer implements IInterpreterComparer { if (isProblematicCondaEnvironment(i)) { return false; } - if (workspaceUri && isActiveStateEnvironmentForWorkspace(i.path, workspaceUri.folderUri.fsPath)) { + if (i.path && workspaceUri && isActiveStateEnvironmentForWorkspace(i.path, workspaceUri.folderUri.fsPath)) { return true; } if (getEnvLocationHeuristic(i, workspaceUri?.folderUri.fsPath || '') === EnvLocationHeuristic.Local) { From 1d135ae348ca5e6af60e9af7a45b7c57eb6bbbae Mon Sep 17 00:00:00 2001 From: mitchell Date: Wed, 25 Jan 2023 15:39:18 -0500 Subject: [PATCH 12/20] Added preference for specifying path to ActiveState's State Tool. --- package.json | 6 +++++ package.nls.json | 1 + resources/report_issue_user_settings.json | 1 + src/client/common/configSettings.ts | 4 ++++ src/client/common/types.ts | 1 + .../common/environmentManagers/activestate.ts | 23 +++++++++++++++---- .../configSettings.unit.test.ts | 3 ++- .../lowLevel/activestateLocator.unit.test.ts | 2 ++ 8 files changed, 36 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index b820542b7728..2cffc56ffc21 100644 --- a/package.json +++ b/package.json @@ -948,6 +948,12 @@ "type": "string", "deprecationMessage": "%python.sortImports.path.deprecationMessage%" }, + "python.statePath": { + "default": "state", + "description": "%python.statePath.description%", + "scope": "machine-overridable", + "type": "string" + }, "python.tensorBoard.logDirectory": { "default": "", "description": "%python.tensorBoard.logDirectory.description%", diff --git a/package.nls.json b/package.nls.json index 82d293e74da4..2d8540a46de3 100644 --- a/package.nls.json +++ b/package.nls.json @@ -96,6 +96,7 @@ "python.poetryPath.description": "Path to the poetry executable.", "python.sortImports.args.description": "Arguments passed in. Each argument is a separate item in the array.", "python.sortImports.path.description": "Path to isort script, default using inner version", + "python.statePath.description": "Path to the State Tool executable for ActiveState runtimes (version 0.36+).", "python.tensorBoard.logDirectory.description": "Set this setting to your preferred TensorBoard log directory to skip log directory prompt when starting TensorBoard.", "python.terminal.activateEnvInCurrentTerminal.description": "Activate Python Environment in the current Terminal on load of the Extension.", "python.terminal.activateEnvironment.description": "Activate Python Environment in Terminal created using the Extension.", diff --git a/resources/report_issue_user_settings.json b/resources/report_issue_user_settings.json index c8d5d743275e..2b02054e5408 100644 --- a/resources/report_issue_user_settings.json +++ b/resources/report_issue_user_settings.json @@ -10,6 +10,7 @@ "condaPath": "placeholder", "pipenvPath": "placeholder", "poetryPath": "placeholder", + "statePath": "placeholder", "devOptions": false, "globalModuleInstallation": false, "languageServer": true, diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 6c31de4361c8..50f6ded3020c 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -104,6 +104,8 @@ export class PythonSettings implements IPythonSettings { public poetryPath = ''; + public statePath = ''; + public devOptions: string[] = []; public linting!: ILintingSettings; @@ -260,6 +262,8 @@ export class PythonSettings implements IPythonSettings { this.pipenvPath = pipenvPath && pipenvPath.length > 0 ? getAbsolutePath(pipenvPath, workspaceRoot) : pipenvPath; const poetryPath = systemVariables.resolveAny(pythonSettings.get('poetryPath'))!; this.poetryPath = poetryPath && poetryPath.length > 0 ? getAbsolutePath(poetryPath, workspaceRoot) : poetryPath; + const statePath = systemVariables.resolveAny(pythonSettings.get('statePath'))!; + this.statePath = statePath && statePath.length > 0 ? getAbsolutePath(statePath, workspaceRoot) : statePath; this.interpreter = pythonSettings.get('interpreter') ?? { infoVisibility: 'onPythonRelated', diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 886d34a1914b..e3214e7239cb 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -187,6 +187,7 @@ export interface IPythonSettings { readonly condaPath: string; readonly pipenvPath: string; readonly poetryPath: string; + readonly statePath: string; readonly devOptions: string[]; readonly linting: ILintingSettings; readonly formatting: IFormattingSettings; diff --git a/src/client/pythonEnvironments/common/environmentManagers/activestate.ts b/src/client/pythonEnvironments/common/environmentManagers/activestate.ts index d6eff48dd250..e8c3a673beca 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/activestate.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/activestate.ts @@ -5,11 +5,19 @@ import * as path from 'path'; import { dirname } from 'path'; -import { arePathsSame, pathExists, shellExecute } from '../externalDependencies'; +import { + arePathsSame, + getPythonSetting, + onDidChangePythonSetting, + pathExists, + shellExecute, +} from '../externalDependencies'; import { cache } from '../../../common/utils/decorators'; import { traceError, traceVerbose } from '../../../logging'; import { getOSType, getUserHomeDir, OSType } from '../../../common/utils/platform'; +export const STATEPATH_SETTING_KEY = 'statePath'; + const STATE_GENERAL_TIMEOUT = 5000; export type ProjectInfo = { @@ -35,6 +43,12 @@ export class ActiveState { return ActiveState.statePromise; } + constructor() { + onDidChangePythonSetting(STATEPATH_SETTING_KEY, () => { + ActiveState.statePromise = undefined; + }); + } + public static getStateToolDir(): string | undefined { const home = getUserHomeDir(); if (!home) { @@ -57,13 +71,14 @@ export class ActiveState { return this.getProjectsCached(); } - private static readonly stateCommand: string = 'state'; + private static readonly defaultStateCommand: string = 'state'; @cache(30_000, true, 10_000) // eslint-disable-next-line class-methods-use-this private async getProjectsCached(): Promise { try { - const result = await shellExecute(`${ActiveState.stateCommand} projects -o editor`, { + const stateCommand = getPythonSetting(STATEPATH_SETTING_KEY) ?? ActiveState.defaultStateCommand; + const result = await shellExecute(`${stateCommand} projects -o editor`, { timeout: STATE_GENERAL_TIMEOUT, }); if (!result) { @@ -74,7 +89,7 @@ export class ActiveState { // '\0' is a record separator. output = output.substring(0, output.length - 1); } - traceVerbose(`${ActiveState.stateCommand} projects -o editor: ${output}`); + traceVerbose(`${stateCommand} projects -o editor: ${output}`); return JSON.parse(output); } catch (ex) { traceError(ex); diff --git a/src/test/common/configSettings/configSettings.unit.test.ts b/src/test/common/configSettings/configSettings.unit.test.ts index 7d2d6230f05b..41f5b6da0f53 100644 --- a/src/test/common/configSettings/configSettings.unit.test.ts +++ b/src/test/common/configSettings/configSettings.unit.test.ts @@ -85,6 +85,7 @@ suite('Python Settings', async () => { 'pipenvPath', 'envFile', 'poetryPath', + 'statePath', 'defaultInterpreterPath', ]) { config @@ -139,7 +140,7 @@ suite('Python Settings', async () => { } suite('String settings', async () => { - ['venvPath', 'condaPath', 'pipenvPath', 'envFile', 'poetryPath', 'defaultInterpreterPath'].forEach( + ['venvPath', 'condaPath', 'pipenvPath', 'envFile', 'poetryPath', 'statePath', 'defaultInterpreterPath'].forEach( async (settingName) => { testIfValueIsUpdated(settingName, 'stringValue'); }, diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts index cef458fe3c04..081cd19f4bb6 100644 --- a/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts @@ -40,6 +40,8 @@ suite('ActiveState Locator', () => { sinon.stub(fsapi, 'pathExists').callsFake((dir: string) => dir === stateToolDir); } + sinon.stub(externalDependencies, 'getPythonSetting').returns(undefined); + sinon.stub(externalDependencies, 'shellExecute').callsFake((command: string) => { if (command === 'state projects -o editor') { return Promise.resolve>({ From 31545bd3b101412aa410bfb39e87fdfbc0f38421 Mon Sep 17 00:00:00 2001 From: mitchell Date: Tue, 31 Jan 2023 13:15:32 -0500 Subject: [PATCH 13/20] Fixed failing Windows unit test. --- .../lowLevel/activestateLocator.unit.test.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts index 081cd19f4bb6..c25df21f7ed4 100644 --- a/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts @@ -45,15 +45,14 @@ suite('ActiveState Locator', () => { sinon.stub(externalDependencies, 'shellExecute').callsFake((command: string) => { if (command === 'state projects -o editor') { return Promise.resolve>({ - stdout: `[{"name":"test","organization":"test-org","local_checkouts":["does-not-matter"],"executables":["${path.join( - testActiveStateDir, - 'c09080d1', - 'exec', - )}"]},{"name":"test2","organization":"test-org","local_checkouts":["does-not-matter2"],"executables":["${path.join( - testActiveStateDir, - '2af6390a', - 'exec', - )}"]}]\n\0`, + stdout: `[{"name":"test","organization":"test-org","local_checkouts":["does-not-matter"],"executables":["${path + .join(testActiveStateDir, 'c09080d1', 'exec') + .replaceAll( + '\\', + '\\\\', + )}"]},{"name":"test2","organization":"test-org","local_checkouts":["does-not-matter2"],"executables":["${path + .join(testActiveStateDir, '2af6390a', 'exec') + .replaceAll('\\', '\\\\')}"]}]\n\0`, }); } return Promise.reject(new Error('Command failed')); From 63098065663f065f103fd83133cc6935fbfe45d6 Mon Sep 17 00:00:00 2001 From: mitchell Date: Tue, 31 Jan 2023 15:05:32 -0500 Subject: [PATCH 14/20] Do not rely on receiver to cache project info. --- .../base/locators/lowLevel/activestateLocator.ts | 1 - .../common/environmentManagers/activestate.ts | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/activestateLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/activestateLocator.ts index e889d5eb2cd1..315f8e283d85 100644 --- a/src/client/pythonEnvironments/base/locators/lowLevel/activestateLocator.ts +++ b/src/client/pythonEnvironments/base/locators/lowLevel/activestateLocator.ts @@ -25,7 +25,6 @@ export class ActiveStateLocator extends LazyResourceBasedLocator { traceVerbose(`Couldn't fetch State Tool projects.`); return; } - ActiveState.setCachedProjectInfo(projects); for (const project of projects) { if (project.executables) { for (const dir of project.executables) { diff --git a/src/client/pythonEnvironments/common/environmentManagers/activestate.ts b/src/client/pythonEnvironments/common/environmentManagers/activestate.ts index e8c3a673beca..7aa60b802ba7 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/activestate.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/activestate.ts @@ -90,7 +90,9 @@ export class ActiveState { output = output.substring(0, output.length - 1); } traceVerbose(`${stateCommand} projects -o editor: ${output}`); - return JSON.parse(output); + const projects = JSON.parse(output); + ActiveState.setCachedProjectInfo(projects); + return projects; } catch (ex) { traceError(ex); return undefined; @@ -106,7 +108,7 @@ export class ActiveState { return this.cachedProjectInfo; } - public static setCachedProjectInfo(projects: ProjectInfo[]): void { + private static setCachedProjectInfo(projects: ProjectInfo[]): void { this.cachedProjectInfo = projects; } } From 9b01ec05483a0b7dfb83dfd42f4a11b2110d9ce8 Mon Sep 17 00:00:00 2001 From: mitchell Date: Tue, 31 Jan 2023 16:00:26 -0500 Subject: [PATCH 15/20] ActiveState runtimes are not virtual environments. --- src/client/pythonEnvironments/base/info/index.ts | 1 - src/client/pythonEnvironments/info/index.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/client/pythonEnvironments/base/info/index.ts b/src/client/pythonEnvironments/base/info/index.ts index ddf0d2fbccc0..e55031fe8078 100644 --- a/src/client/pythonEnvironments/base/info/index.ts +++ b/src/client/pythonEnvironments/base/info/index.ts @@ -49,7 +49,6 @@ export const virtualEnvKinds = [ PythonEnvKind.VirtualEnvWrapper, PythonEnvKind.Conda, PythonEnvKind.VirtualEnv, - PythonEnvKind.ActiveState, ]; export const globallyInstalledEnvKinds = [ diff --git a/src/client/pythonEnvironments/info/index.ts b/src/client/pythonEnvironments/info/index.ts index 1ee8b46202f7..7ff34fc60834 100644 --- a/src/client/pythonEnvironments/info/index.ts +++ b/src/client/pythonEnvironments/info/index.ts @@ -31,7 +31,6 @@ export const virtualEnvTypes = [ EnvironmentType.VirtualEnvWrapper, EnvironmentType.Conda, EnvironmentType.VirtualEnv, - EnvironmentType.ActiveState, ]; /** From 8083090d7e314b6f3cef4a5cf35ee343b2068270 Mon Sep 17 00:00:00 2001 From: mitchell Date: Tue, 31 Jan 2023 16:01:22 -0500 Subject: [PATCH 16/20] Updated conditional based on PR feedback. --- .../interpreter/configuration/environmentTypeComparer.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/client/interpreter/configuration/environmentTypeComparer.ts b/src/client/interpreter/configuration/environmentTypeComparer.ts index d8431c9a8eb0..a8d6ce293a1b 100644 --- a/src/client/interpreter/configuration/environmentTypeComparer.ts +++ b/src/client/interpreter/configuration/environmentTypeComparer.ts @@ -94,8 +94,13 @@ export class EnvironmentTypeComparer implements IInterpreterComparer { if (isProblematicCondaEnvironment(i)) { return false; } - if (i.path && workspaceUri && isActiveStateEnvironmentForWorkspace(i.path, workspaceUri.folderUri.fsPath)) { - return true; + if ( + i.envType === EnvironmentType.ActiveState && + (!i.path || + !workspaceUri || + !isActiveStateEnvironmentForWorkspace(i.path, workspaceUri.folderUri.fsPath)) + ) { + return false; } if (getEnvLocationHeuristic(i, workspaceUri?.folderUri.fsPath || '') === EnvLocationHeuristic.Local) { return true; From c0ea1be395efcba73764f85a6122d68517107aae Mon Sep 17 00:00:00 2001 From: mitchell Date: Thu, 2 Feb 2023 11:21:13 -0500 Subject: [PATCH 17/20] PR feedback. --- src/client/pythonEnvironments/info/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/client/pythonEnvironments/info/index.ts b/src/client/pythonEnvironments/info/index.ts index 7ff34fc60834..70abbb0fad76 100644 --- a/src/client/pythonEnvironments/info/index.ts +++ b/src/client/pythonEnvironments/info/index.ts @@ -42,7 +42,6 @@ export enum ModuleInstallerType { Pip = 'Pip', Poetry = 'Poetry', Pipenv = 'Pipenv', - ActiveState = 'ActiveState', } /** From e90ae165b3327176c2f95bdff20cd70f19b3ebdb Mon Sep 17 00:00:00 2001 From: mitchell Date: Thu, 2 Feb 2023 11:24:07 -0500 Subject: [PATCH 18/20] Revert: Added preference for specifying path to ActiveState's State Tool. --- package.json | 6 ----- package.nls.json | 1 - resources/report_issue_user_settings.json | 1 - src/client/common/configSettings.ts | 4 ---- src/client/common/types.ts | 1 - .../common/environmentManagers/activestate.ts | 23 ++++--------------- .../configSettings.unit.test.ts | 3 +-- .../lowLevel/activestateLocator.unit.test.ts | 2 -- 8 files changed, 5 insertions(+), 36 deletions(-) diff --git a/package.json b/package.json index c2f56b8658cf..399be812f06e 100644 --- a/package.json +++ b/package.json @@ -934,12 +934,6 @@ "type": "string", "deprecationMessage": "%python.sortImports.path.deprecationMessage%" }, - "python.statePath": { - "default": "state", - "description": "%python.statePath.description%", - "scope": "machine-overridable", - "type": "string" - }, "python.tensorBoard.logDirectory": { "default": "", "description": "%python.tensorBoard.logDirectory.description%", diff --git a/package.nls.json b/package.nls.json index 71d8719679b5..459346744336 100644 --- a/package.nls.json +++ b/package.nls.json @@ -96,7 +96,6 @@ "python.poetryPath.description": "Path to the poetry executable.", "python.sortImports.args.description": "Arguments passed in. Each argument is a separate item in the array.", "python.sortImports.path.description": "Path to isort script, default using inner version", - "python.statePath.description": "Path to the State Tool executable for ActiveState runtimes (version 0.36+).", "python.tensorBoard.logDirectory.description": "Set this setting to your preferred TensorBoard log directory to skip log directory prompt when starting TensorBoard.", "python.terminal.activateEnvInCurrentTerminal.description": "Activate Python Environment in the current Terminal on load of the Extension.", "python.terminal.activateEnvironment.description": "Activate Python Environment in Terminal created using the Extension.", diff --git a/resources/report_issue_user_settings.json b/resources/report_issue_user_settings.json index 2b02054e5408..c8d5d743275e 100644 --- a/resources/report_issue_user_settings.json +++ b/resources/report_issue_user_settings.json @@ -10,7 +10,6 @@ "condaPath": "placeholder", "pipenvPath": "placeholder", "poetryPath": "placeholder", - "statePath": "placeholder", "devOptions": false, "globalModuleInstallation": false, "languageServer": true, diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 50f6ded3020c..6c31de4361c8 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -104,8 +104,6 @@ export class PythonSettings implements IPythonSettings { public poetryPath = ''; - public statePath = ''; - public devOptions: string[] = []; public linting!: ILintingSettings; @@ -262,8 +260,6 @@ export class PythonSettings implements IPythonSettings { this.pipenvPath = pipenvPath && pipenvPath.length > 0 ? getAbsolutePath(pipenvPath, workspaceRoot) : pipenvPath; const poetryPath = systemVariables.resolveAny(pythonSettings.get('poetryPath'))!; this.poetryPath = poetryPath && poetryPath.length > 0 ? getAbsolutePath(poetryPath, workspaceRoot) : poetryPath; - const statePath = systemVariables.resolveAny(pythonSettings.get('statePath'))!; - this.statePath = statePath && statePath.length > 0 ? getAbsolutePath(statePath, workspaceRoot) : statePath; this.interpreter = pythonSettings.get('interpreter') ?? { infoVisibility: 'onPythonRelated', diff --git a/src/client/common/types.ts b/src/client/common/types.ts index e3214e7239cb..886d34a1914b 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -187,7 +187,6 @@ export interface IPythonSettings { readonly condaPath: string; readonly pipenvPath: string; readonly poetryPath: string; - readonly statePath: string; readonly devOptions: string[]; readonly linting: ILintingSettings; readonly formatting: IFormattingSettings; diff --git a/src/client/pythonEnvironments/common/environmentManagers/activestate.ts b/src/client/pythonEnvironments/common/environmentManagers/activestate.ts index 7aa60b802ba7..678bc04c1317 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/activestate.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/activestate.ts @@ -5,19 +5,11 @@ import * as path from 'path'; import { dirname } from 'path'; -import { - arePathsSame, - getPythonSetting, - onDidChangePythonSetting, - pathExists, - shellExecute, -} from '../externalDependencies'; +import { arePathsSame, pathExists, shellExecute } from '../externalDependencies'; import { cache } from '../../../common/utils/decorators'; import { traceError, traceVerbose } from '../../../logging'; import { getOSType, getUserHomeDir, OSType } from '../../../common/utils/platform'; -export const STATEPATH_SETTING_KEY = 'statePath'; - const STATE_GENERAL_TIMEOUT = 5000; export type ProjectInfo = { @@ -43,12 +35,6 @@ export class ActiveState { return ActiveState.statePromise; } - constructor() { - onDidChangePythonSetting(STATEPATH_SETTING_KEY, () => { - ActiveState.statePromise = undefined; - }); - } - public static getStateToolDir(): string | undefined { const home = getUserHomeDir(); if (!home) { @@ -71,14 +57,13 @@ export class ActiveState { return this.getProjectsCached(); } - private static readonly defaultStateCommand: string = 'state'; + private static readonly stateCommand: string = 'state'; @cache(30_000, true, 10_000) // eslint-disable-next-line class-methods-use-this private async getProjectsCached(): Promise { try { - const stateCommand = getPythonSetting(STATEPATH_SETTING_KEY) ?? ActiveState.defaultStateCommand; - const result = await shellExecute(`${stateCommand} projects -o editor`, { + const result = await shellExecute(`${ActiveState.stateCommand} projects -o editor`, { timeout: STATE_GENERAL_TIMEOUT, }); if (!result) { @@ -89,7 +74,7 @@ export class ActiveState { // '\0' is a record separator. output = output.substring(0, output.length - 1); } - traceVerbose(`${stateCommand} projects -o editor: ${output}`); + traceVerbose(`${ActiveState.stateCommand} projects -o editor: ${output}`); const projects = JSON.parse(output); ActiveState.setCachedProjectInfo(projects); return projects; diff --git a/src/test/common/configSettings/configSettings.unit.test.ts b/src/test/common/configSettings/configSettings.unit.test.ts index 41f5b6da0f53..7d2d6230f05b 100644 --- a/src/test/common/configSettings/configSettings.unit.test.ts +++ b/src/test/common/configSettings/configSettings.unit.test.ts @@ -85,7 +85,6 @@ suite('Python Settings', async () => { 'pipenvPath', 'envFile', 'poetryPath', - 'statePath', 'defaultInterpreterPath', ]) { config @@ -140,7 +139,7 @@ suite('Python Settings', async () => { } suite('String settings', async () => { - ['venvPath', 'condaPath', 'pipenvPath', 'envFile', 'poetryPath', 'statePath', 'defaultInterpreterPath'].forEach( + ['venvPath', 'condaPath', 'pipenvPath', 'envFile', 'poetryPath', 'defaultInterpreterPath'].forEach( async (settingName) => { testIfValueIsUpdated(settingName, 'stringValue'); }, diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts index c25df21f7ed4..5e15c3b0a734 100644 --- a/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts @@ -40,8 +40,6 @@ suite('ActiveState Locator', () => { sinon.stub(fsapi, 'pathExists').callsFake((dir: string) => dir === stateToolDir); } - sinon.stub(externalDependencies, 'getPythonSetting').returns(undefined); - sinon.stub(externalDependencies, 'shellExecute').callsFake((command: string) => { if (command === 'state projects -o editor') { return Promise.resolve>({ From b5595b2a6b0868ad5945149ac2c5881158da5bc6 Mon Sep 17 00:00:00 2001 From: mitchell Date: Tue, 7 Feb 2023 13:47:57 -0500 Subject: [PATCH 19/20] Re-added preference for specifying path to ActiveState's State Tool. Use a more descriptive name. --- package.json | 6 ++++ package.nls.json | 1 + resources/report_issue_user_settings.json | 1 + src/client/common/configSettings.ts | 7 +++++ src/client/common/types.ts | 1 + .../common/environmentManagers/activestate.ts | 28 +++++++++++++++---- .../configSettings.unit.test.ts | 17 +++++++---- .../lowLevel/activestateLocator.unit.test.ts | 2 ++ 8 files changed, 53 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 399be812f06e..1dd275cf44ca 100644 --- a/package.json +++ b/package.json @@ -376,6 +376,12 @@ ], "configuration": { "properties": { + "python.activeStateToolPath": { + "default": "state", + "description": "%python.activeStateToolPath.description%", + "scope": "machine-overridable", + "type": "string" + }, "python.autoComplete.extraPaths": { "default": [], "description": "%python.autoComplete.extraPaths.description%", diff --git a/package.nls.json b/package.nls.json index 459346744336..02eaad9f6a07 100644 --- a/package.nls.json +++ b/package.nls.json @@ -25,6 +25,7 @@ "python.command.python.launchTensorBoard.title": "Launch TensorBoard", "python.command.python.refreshTensorBoard.title": "Refresh TensorBoard", "python.menu.createNewFile.title": "Python File", + "python.activeStateToolPath.description": "Path to the State Tool executable for ActiveState runtimes (version 0.36+).", "python.autoComplete.extraPaths.description": "List of paths to libraries and the like that need to be imported by auto complete engine. E.g. when using Google App SDK, the paths are not in system path, hence need to be added into this list.", "python.condaPath.description": "Path to the conda executable to use for activation (version 4.4+).", "python.defaultInterpreterPath.description": "Path to default Python to use when extension loads up for the first time, no longer used once an interpreter is selected for the workspace. See [here](https://aka.ms/AAfekmf) to understand when this is used", diff --git a/resources/report_issue_user_settings.json b/resources/report_issue_user_settings.json index c8d5d743275e..778434c5cf0d 100644 --- a/resources/report_issue_user_settings.json +++ b/resources/report_issue_user_settings.json @@ -7,6 +7,7 @@ "envFile": "placeholder", "venvPath": "placeholder", "venvFolders": "placeholder", + "activeStateToolPath": "placeholder", "condaPath": "placeholder", "pipenvPath": "placeholder", "poetryPath": "placeholder", diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 6c31de4361c8..3e3525d5b2a4 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -98,6 +98,8 @@ export class PythonSettings implements IPythonSettings { public venvFolders: string[] = []; + public activeStateToolPath = ''; + public condaPath = ''; public pipenvPath = ''; @@ -254,6 +256,11 @@ export class PythonSettings implements IPythonSettings { this.venvPath = systemVariables.resolveAny(pythonSettings.get('venvPath'))!; this.venvFolders = systemVariables.resolveAny(pythonSettings.get('venvFolders'))!; + const activeStateToolPath = systemVariables.resolveAny(pythonSettings.get('activeStateToolPath'))!; + this.activeStateToolPath = + activeStateToolPath && activeStateToolPath.length > 0 + ? getAbsolutePath(activeStateToolPath, workspaceRoot) + : activeStateToolPath; const condaPath = systemVariables.resolveAny(pythonSettings.get('condaPath'))!; this.condaPath = condaPath && condaPath.length > 0 ? getAbsolutePath(condaPath, workspaceRoot) : condaPath; const pipenvPath = systemVariables.resolveAny(pythonSettings.get('pipenvPath'))!; diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 886d34a1914b..8f340c3e01e2 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -184,6 +184,7 @@ export interface IPythonSettings { readonly pythonPath: string; readonly venvPath: string; readonly venvFolders: string[]; + readonly activeStateToolPath: string; readonly condaPath: string; readonly pipenvPath: string; readonly poetryPath: string; diff --git a/src/client/pythonEnvironments/common/environmentManagers/activestate.ts b/src/client/pythonEnvironments/common/environmentManagers/activestate.ts index 678bc04c1317..75b34f41176c 100644 --- a/src/client/pythonEnvironments/common/environmentManagers/activestate.ts +++ b/src/client/pythonEnvironments/common/environmentManagers/activestate.ts @@ -5,11 +5,19 @@ import * as path from 'path'; import { dirname } from 'path'; -import { arePathsSame, pathExists, shellExecute } from '../externalDependencies'; +import { + arePathsSame, + getPythonSetting, + onDidChangePythonSetting, + pathExists, + shellExecute, +} from '../externalDependencies'; import { cache } from '../../../common/utils/decorators'; import { traceError, traceVerbose } from '../../../logging'; import { getOSType, getUserHomeDir, OSType } from '../../../common/utils/platform'; +export const ACTIVESTATETOOLPATH_SETTING_KEY = 'activeStateToolPath'; + const STATE_GENERAL_TIMEOUT = 5000; export type ProjectInfo = { @@ -35,6 +43,12 @@ export class ActiveState { return ActiveState.statePromise; } + constructor() { + onDidChangePythonSetting(ACTIVESTATETOOLPATH_SETTING_KEY, () => { + ActiveState.statePromise = undefined; + }); + } + public static getStateToolDir(): string | undefined { const home = getUserHomeDir(); if (!home) { @@ -47,7 +61,9 @@ export class ActiveState { private static async locate(): Promise { const stateToolDir = this.getStateToolDir(); - if (stateToolDir && (await pathExists(stateToolDir))) { + const stateCommand = + getPythonSetting(ACTIVESTATETOOLPATH_SETTING_KEY) ?? ActiveState.defaultStateCommand; + if (stateToolDir && ((await pathExists(stateToolDir)) || stateCommand !== this.defaultStateCommand)) { return new ActiveState(); } return undefined; @@ -57,13 +73,15 @@ export class ActiveState { return this.getProjectsCached(); } - private static readonly stateCommand: string = 'state'; + private static readonly defaultStateCommand: string = 'state'; @cache(30_000, true, 10_000) // eslint-disable-next-line class-methods-use-this private async getProjectsCached(): Promise { try { - const result = await shellExecute(`${ActiveState.stateCommand} projects -o editor`, { + const stateCommand = + getPythonSetting(ACTIVESTATETOOLPATH_SETTING_KEY) ?? ActiveState.defaultStateCommand; + const result = await shellExecute(`${stateCommand} projects -o editor`, { timeout: STATE_GENERAL_TIMEOUT, }); if (!result) { @@ -74,7 +92,7 @@ export class ActiveState { // '\0' is a record separator. output = output.substring(0, output.length - 1); } - traceVerbose(`${ActiveState.stateCommand} projects -o editor: ${output}`); + traceVerbose(`${stateCommand} projects -o editor: ${output}`); const projects = JSON.parse(output); ActiveState.setCachedProjectInfo(projects); return projects; diff --git a/src/test/common/configSettings/configSettings.unit.test.ts b/src/test/common/configSettings/configSettings.unit.test.ts index 7d2d6230f05b..eeaed6aa996b 100644 --- a/src/test/common/configSettings/configSettings.unit.test.ts +++ b/src/test/common/configSettings/configSettings.unit.test.ts @@ -81,6 +81,7 @@ suite('Python Settings', async () => { for (const name of [ 'pythonPath', 'venvPath', + 'activeStateToolPath', 'condaPath', 'pipenvPath', 'envFile', @@ -139,11 +140,17 @@ suite('Python Settings', async () => { } suite('String settings', async () => { - ['venvPath', 'condaPath', 'pipenvPath', 'envFile', 'poetryPath', 'defaultInterpreterPath'].forEach( - async (settingName) => { - testIfValueIsUpdated(settingName, 'stringValue'); - }, - ); + [ + 'venvPath', + 'activeStateToolPath', + 'condaPath', + 'pipenvPath', + 'envFile', + 'poetryPath', + 'defaultInterpreterPath', + ].forEach(async (settingName) => { + testIfValueIsUpdated(settingName, 'stringValue'); + }); }); suite('Boolean settings', async () => { diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts index 5e15c3b0a734..c25df21f7ed4 100644 --- a/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts @@ -40,6 +40,8 @@ suite('ActiveState Locator', () => { sinon.stub(fsapi, 'pathExists').callsFake((dir: string) => dir === stateToolDir); } + sinon.stub(externalDependencies, 'getPythonSetting').returns(undefined); + sinon.stub(externalDependencies, 'shellExecute').callsFake((command: string) => { if (command === 'state projects -o editor') { return Promise.resolve>({ From 6eb0ac3c334e1b5b712c3fc60d4276cde56b2c4e Mon Sep 17 00:00:00 2001 From: mitchell Date: Tue, 7 Feb 2023 14:44:33 -0500 Subject: [PATCH 20/20] Fix lint error. --- .../lowLevel/activestateLocator.unit.test.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts index c25df21f7ed4..5bdbd22def0f 100644 --- a/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts +++ b/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts @@ -14,6 +14,7 @@ import { ExecutionResult } from '../../../../../client/common/process/types'; import { createBasicEnv } from '../../common'; import * as platform from '../../../../../client/common/utils/platform'; import { ActiveState } from '../../../../../client/pythonEnvironments/common/environmentManagers/activestate'; +import { replaceAll } from '../../../../../client/common/stringUtils'; suite('ActiveState Locator', () => { const testActiveStateDir = path.join(TEST_LAYOUT_ROOT, 'activestate'); @@ -45,14 +46,15 @@ suite('ActiveState Locator', () => { sinon.stub(externalDependencies, 'shellExecute').callsFake((command: string) => { if (command === 'state projects -o editor') { return Promise.resolve>({ - stdout: `[{"name":"test","organization":"test-org","local_checkouts":["does-not-matter"],"executables":["${path - .join(testActiveStateDir, 'c09080d1', 'exec') - .replaceAll( - '\\', - '\\\\', - )}"]},{"name":"test2","organization":"test-org","local_checkouts":["does-not-matter2"],"executables":["${path - .join(testActiveStateDir, '2af6390a', 'exec') - .replaceAll('\\', '\\\\')}"]}]\n\0`, + stdout: `[{"name":"test","organization":"test-org","local_checkouts":["does-not-matter"],"executables":["${replaceAll( + path.join(testActiveStateDir, 'c09080d1', 'exec'), + '\\', + '\\\\', + )}"]},{"name":"test2","organization":"test-org","local_checkouts":["does-not-matter2"],"executables":["${replaceAll( + path.join(testActiveStateDir, '2af6390a', 'exec'), + '\\', + '\\\\', + )}"]}]\n\0`, }); } return Promise.reject(new Error('Command failed'));