Skip to content

Commit

Permalink
Detect ActiveState Python runtimes (#20532)
Browse files Browse the repository at this point in the history
  • Loading branch information
mitchell-as committed Jan 23, 2023
1 parent bebf05d commit d6ba0a7
Show file tree
Hide file tree
Showing 22 changed files with 243 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ function getPrioritizedEnvironmentType(): EnvironmentType[] {
EnvironmentType.VirtualEnvWrapper,
EnvironmentType.Venv,
EnvironmentType.VirtualEnv,
EnvironmentType.ActiveState,
EnvironmentType.Conda,
EnvironmentType.Pyenv,
EnvironmentType.MicrosoftStore,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
2 changes: 2 additions & 0 deletions src/client/pythonEnvironments/base/info/envKind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -63,6 +64,7 @@ export function getPrioritizedEnvKinds(): PythonEnvKind[] {
PythonEnvKind.Venv,
PythonEnvKind.VirtualEnvWrapper,
PythonEnvKind.VirtualEnv,
PythonEnvKind.ActiveState,
PythonEnvKind.OtherVirtual,
PythonEnvKind.OtherGlobal,
PythonEnvKind.System,
Expand Down
3 changes: 3 additions & 0 deletions src/client/pythonEnvironments/base/info/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -28,6 +29,7 @@ export enum PythonEnvKind {

export enum PythonEnvType {
Conda = 'Conda',
ActiveState = 'ActiveState',
Virtual = 'Virtual',
}

Expand All @@ -48,6 +50,7 @@ export const virtualEnvKinds = [
PythonEnvKind.VirtualEnvWrapper,
PythonEnvKind.Conda,
PythonEnvKind.VirtualEnv,
PythonEnvKind.ActiveState,
];

export const globallyInstalledEnvKinds = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<PythonEnvKind, (env: BasicEnvInfo) => Promise<PythonEnvInfo>> {
const resolvers = new Map<PythonEnvKind, (_: BasicEnvInfo) => Promise<PythonEnvInfo>>();
Expand All @@ -37,6 +38,7 @@ function getResolvers(): Map<PythonEnvKind, (env: BasicEnvInfo) => Promise<Pytho
resolvers.set(PythonEnvKind.Conda, resolveCondaEnv);
resolvers.set(PythonEnvKind.MicrosoftStore, resolveMicrosoftStoreEnv);
resolvers.set(PythonEnvKind.Pyenv, resolvePyenvEnv);
resolvers.set(PythonEnvKind.ActiveState, resolveActiveStateEnv);
return resolvers;
}

Expand Down Expand Up @@ -78,6 +80,9 @@ 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;
}

Expand Down Expand Up @@ -236,6 +241,26 @@ async function resolvePyenvEnv(env: BasicEnvInfo): Promise<PythonEnvInfo> {
return envInfo;
}

async function resolveActiveStateEnv(env: BasicEnvInfo): Promise<PythonEnvInfo> {
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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<BasicEnvInfo> {
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);
}
}
}
}
}
}
2 changes: 2 additions & 0 deletions src/client/pythonEnvironments/common/environmentIdentifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
isVirtualenvwrapperEnvironment as isVirtualEnvWrapperEnvironment,
} from './environmentManagers/simplevirtualenvs';
import { isMicrosoftStoreEnvironment } from './environmentManagers/microsoftStoreEnv';
import { isActiveStateEnvironment } from './environmentManagers/activestate';

function getIdentifiers(): Map<PythonEnvKind, (path: string) => Promise<boolean>> {
const notImplemented = () => Promise.resolve(false);
Expand All @@ -32,6 +33,7 @@ function getIdentifiers(): Map<PythonEnvKind, (path: string) => Promise<boolean>
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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<boolean> {
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<ProjectInfo[] | undefined> {
return this.getProjectsCached();
}

@cache(30_000, true, 10_000)
private static async getProjectsCached(): Promise<ProjectInfo[] | undefined> {
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;
}
}
}
2 changes: 2 additions & 0 deletions src/client/pythonEnvironments/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).'
Expand Down Expand Up @@ -137,6 +138,7 @@ function createNonWorkspaceLocators(ext: ExtensionState): ILocator<BasicEnvInfo>
// OS-independent locators go here.
new PyenvLocator(),
new CondaEnvironmentLocator(),
new ActiveStateLocator(),
new GlobalVirtualEnvironmentLocator(),
new CustomVirtualEnvironmentLocator(),
);
Expand Down
6 changes: 6 additions & 0 deletions src/client/pythonEnvironments/info/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export enum EnvironmentType {
MicrosoftStore = 'MicrosoftStore',
Poetry = 'Poetry',
VirtualEnvWrapper = 'VirtualEnvWrapper',
ActiveState = 'ActiveState',
Global = 'Global',
System = 'System',
}
Expand All @@ -30,6 +31,7 @@ export const virtualEnvTypes = [
EnvironmentType.VirtualEnvWrapper,
EnvironmentType.Conda,
EnvironmentType.VirtualEnv,
EnvironmentType.ActiveState,
];

/**
Expand All @@ -41,6 +43,7 @@ export enum ModuleInstallerType {
Pip = 'Pip',
Poetry = 'Poetry',
Pipenv = 'Pipenv',
ActiveState = 'ActiveState',
}

/**
Expand Down Expand Up @@ -114,6 +117,9 @@ export function getEnvironmentTypeName(environmentType: EnvironmentType): string
case EnvironmentType.VirtualEnvWrapper: {
return 'virtualenvwrapper';
}
case EnvironmentType.ActiveState: {
return 'activestate';
}
default: {
return '';
}
Expand Down
1 change: 1 addition & 0 deletions src/client/pythonEnvironments/legacyIOC.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const convertedKinds = new Map(
[PythonEnvKind.Poetry]: EnvironmentType.Poetry,
[PythonEnvKind.Venv]: EnvironmentType.Venv,
[PythonEnvKind.VirtualEnvWrapper]: EnvironmentType.VirtualEnvWrapper,
[PythonEnvKind.ActiveState]: EnvironmentType.ActiveState,
}),
);

Expand Down
1 change: 1 addition & 0 deletions src/test/pythonEnvironments/base/info/envKind.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const KIND_NAMES: [PythonEnvKind, string][] = [
[PythonEnvKind.VirtualEnvWrapper, 'virtualenvWrapper'],
[PythonEnvKind.Pipenv, 'pipenv'],
[PythonEnvKind.Conda, 'conda'],
[PythonEnvKind.ActiveState, 'activestate'],
[PythonEnvKind.OtherVirtual, 'otherVirtual'],
];

Expand Down
Original file line number Diff line number Diff line change
@@ -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<ExecutionResult<string>>({
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);
});
});
Original file line number Diff line number Diff line change
@@ -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<IFileSystem>;

setup(() => {
fileSystem = TypeMoq.Mock.ofType<IFileSystem>();
});

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);
});
});
Empty file.
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
invalid python interpreter: missing _runtime_store
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
invalid python interpreter: missing _runtime_store
Empty file.
Empty file.
Empty file.

0 comments on commit d6ba0a7

Please sign in to comment.