Skip to content

Commit a39ba90

Browse files
committed
Detect ActiveState Python runtimes (microsoft#20532)
1 parent f8bce89 commit a39ba90

File tree

23 files changed

+244
-1
lines changed

23 files changed

+244
-1
lines changed

pythonFiles/install_debugpy.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
DEBUGGER_DEST = os.path.join(EXTENSION_ROOT, "pythonFiles", "lib", "python")
1414
DEBUGGER_PACKAGE = "debugpy"
1515
DEBUGGER_PYTHON_ABI_VERSIONS = ("cp310",)
16-
DEBUGGER_VERSION = "1.6.3" # can also be "latest"
16+
DEBUGGER_VERSION = "1.6.5" # can also be "latest"
1717

1818

1919
def _contains(s, parts=()):

src/client/interpreter/configuration/environmentTypeComparer.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ function getPrioritizedEnvironmentType(): EnvironmentType[] {
234234
EnvironmentType.VirtualEnvWrapper,
235235
EnvironmentType.Venv,
236236
EnvironmentType.VirtualEnv,
237+
EnvironmentType.ActiveState,
237238
EnvironmentType.Conda,
238239
EnvironmentType.Pyenv,
239240
EnvironmentType.MicrosoftStore,

src/client/interpreter/configuration/interpreterSelector/commands/setInterpreter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export namespace EnvGroups {
6565
export const Venv = 'Venv';
6666
export const Poetry = 'Poetry';
6767
export const VirtualEnvWrapper = 'VirtualEnvWrapper';
68+
export const ActiveState = 'ActiveState';
6869
export const Recommended = Common.recommended;
6970
}
7071

src/client/pythonEnvironments/base/info/envKind.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export function getKindDisplayName(kind: PythonEnvKind): string {
2222
[PythonEnvKind.VirtualEnvWrapper, 'virtualenv'],
2323
[PythonEnvKind.Pipenv, 'pipenv'],
2424
[PythonEnvKind.Conda, 'conda'],
25+
[PythonEnvKind.ActiveState, 'ActiveState'],
2526
// For now we treat OtherVirtual like Unknown.
2627
] as [PythonEnvKind, string][]) {
2728
if (kind === candidate) {
@@ -63,6 +64,7 @@ export function getPrioritizedEnvKinds(): PythonEnvKind[] {
6364
PythonEnvKind.Venv,
6465
PythonEnvKind.VirtualEnvWrapper,
6566
PythonEnvKind.VirtualEnv,
67+
PythonEnvKind.ActiveState,
6668
PythonEnvKind.OtherVirtual,
6769
PythonEnvKind.OtherGlobal,
6870
PythonEnvKind.System,

src/client/pythonEnvironments/base/info/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export enum PythonEnvKind {
1515
MicrosoftStore = 'global-microsoft-store',
1616
Pyenv = 'global-pyenv',
1717
Poetry = 'poetry',
18+
ActiveState = 'activestate',
1819
Custom = 'global-custom',
1920
OtherGlobal = 'global-other',
2021
// "virtual"
@@ -28,6 +29,7 @@ export enum PythonEnvKind {
2829

2930
export enum PythonEnvType {
3031
Conda = 'Conda',
32+
ActiveState = 'ActiveState',
3133
Virtual = 'Virtual',
3234
}
3335

@@ -48,6 +50,7 @@ export const virtualEnvKinds = [
4850
PythonEnvKind.VirtualEnvWrapper,
4951
PythonEnvKind.Conda,
5052
PythonEnvKind.VirtualEnv,
53+
PythonEnvKind.ActiveState,
5154
];
5255

5356
export const globallyInstalledEnvKinds = [

src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { parseVersionFromExecutable } from '../../info/executable';
2525
import { traceError, traceWarn } from '../../../../logging';
2626
import { isVirtualEnvironment } from '../../../common/environmentManagers/simplevirtualenvs';
2727
import { getWorkspaceFolderPaths } from '../../../../common/vscodeApis/workspaceApis';
28+
import { ActiveState, isActiveStateEnvironment } from '../../../common/environmentManagers/activestate';
2829

2930
function getResolvers(): Map<PythonEnvKind, (env: BasicEnvInfo) => Promise<PythonEnvInfo>> {
3031
const resolvers = new Map<PythonEnvKind, (_: BasicEnvInfo) => Promise<PythonEnvInfo>>();
@@ -37,6 +38,7 @@ function getResolvers(): Map<PythonEnvKind, (env: BasicEnvInfo) => Promise<Pytho
3738
resolvers.set(PythonEnvKind.Conda, resolveCondaEnv);
3839
resolvers.set(PythonEnvKind.MicrosoftStore, resolveMicrosoftStoreEnv);
3940
resolvers.set(PythonEnvKind.Pyenv, resolvePyenvEnv);
41+
resolvers.set(PythonEnvKind.ActiveState, resolveActiveStateEnv);
4042
return resolvers;
4143
}
4244

@@ -78,6 +80,9 @@ async function getEnvType(env: PythonEnvInfo) {
7880
if (await isCondaEnvironment(env.executable.filename)) {
7981
return PythonEnvType.Conda;
8082
}
83+
if (await isActiveStateEnvironment(env.executable.filename)) {
84+
return PythonEnvType.ActiveState;
85+
}
8186
return undefined;
8287
}
8388

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

244+
async function resolveActiveStateEnv(env: BasicEnvInfo): Promise<PythonEnvInfo> {
245+
const info = buildEnvInfo({
246+
kind: env.kind,
247+
executable: env.executablePath,
248+
type: PythonEnvType.ActiveState,
249+
});
250+
const projects = await ActiveState.getProjects();
251+
if (projects) {
252+
for (const project of projects) {
253+
for (const dir of project.executables) {
254+
if (dir === path.dirname(env.executablePath)) {
255+
info.name = `${project.organization}/${project.name}`;
256+
return info;
257+
}
258+
}
259+
}
260+
}
261+
return info;
262+
}
263+
239264
async function isBaseCondaPyenvEnvironment(executablePath: string) {
240265
if (!(await isCondaEnvironment(executablePath))) {
241266
return false;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import { ActiveState } from '../../../common/environmentManagers/activestate';
7+
import { PythonEnvKind } from '../../info';
8+
import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator';
9+
import { traceError, traceVerbose } from '../../../../logging';
10+
import { LazyResourceBasedLocator } from '../common/resourceBasedLocator';
11+
import { findInterpretersInDir } from '../../../common/commonUtils';
12+
13+
export class ActiveStateLocator extends LazyResourceBasedLocator {
14+
public readonly providerId: string = 'activestate';
15+
16+
// eslint-disable-next-line class-methods-use-this
17+
public async *doIterEnvs(): IPythonEnvsIterator<BasicEnvInfo> {
18+
const projects = await ActiveState.getProjects();
19+
if (projects === undefined) {
20+
traceVerbose(`Couldn't fetch State Tool projects.`);
21+
return;
22+
}
23+
for (const project of projects) {
24+
if (project.executables) {
25+
for (const dir of project.executables) {
26+
try {
27+
traceVerbose(`Looking for Python in: ${project.name}`);
28+
for await (const exe of findInterpretersInDir(dir)) {
29+
traceVerbose(`Found Python executable: ${exe.filename}`);
30+
yield { kind: PythonEnvKind.ActiveState, executablePath: exe.filename };
31+
}
32+
} catch (ex) {
33+
traceError(`Failed to process State Tool project: ${JSON.stringify(project)}`, ex);
34+
}
35+
}
36+
}
37+
}
38+
}
39+
}

src/client/pythonEnvironments/common/environmentIdentifier.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
isVirtualenvwrapperEnvironment as isVirtualEnvWrapperEnvironment,
1616
} from './environmentManagers/simplevirtualenvs';
1717
import { isMicrosoftStoreEnvironment } from './environmentManagers/microsoftStoreEnv';
18+
import { isActiveStateEnvironment } from './environmentManagers/activestate';
1819

1920
function getIdentifiers(): Map<PythonEnvKind, (path: string) => Promise<boolean>> {
2021
const notImplemented = () => Promise.resolve(false);
@@ -32,6 +33,7 @@ function getIdentifiers(): Map<PythonEnvKind, (path: string) => Promise<boolean>
3233
identifier.set(PythonEnvKind.Venv, isVenvEnvironment);
3334
identifier.set(PythonEnvKind.VirtualEnvWrapper, isVirtualEnvWrapperEnvironment);
3435
identifier.set(PythonEnvKind.VirtualEnv, isVirtualEnvEnvironment);
36+
identifier.set(PythonEnvKind.ActiveState, isActiveStateEnvironment);
3537
identifier.set(PythonEnvKind.Unknown, defaultTrue);
3638
identifier.set(PythonEnvKind.OtherGlobal, isGloballyInstalledEnv);
3739
return identifier;
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
'use strict';
5+
6+
import * as path from 'path';
7+
import { pathExists, shellExecute } from '../externalDependencies';
8+
import { cache } from '../../../common/utils/decorators';
9+
import { traceError, traceVerbose } from '../../../logging';
10+
11+
const STATE_GENERAL_TIMEOUT = 50000;
12+
13+
export type ProjectInfo = {
14+
name: string;
15+
organization: string;
16+
local_checkouts: string[]; // eslint-disable-line camelcase
17+
executables: string[];
18+
};
19+
20+
export async function isActiveStateEnvironment(interpreterPath: string): Promise<boolean> {
21+
const execDir = path.dirname(interpreterPath);
22+
const runtimeDir = path.dirname(execDir);
23+
return pathExists(path.join(runtimeDir, '_runtime_store'));
24+
}
25+
26+
export class ActiveState {
27+
public static readonly stateCommand: string = 'state';
28+
29+
public static async getProjects(): Promise<ProjectInfo[] | undefined> {
30+
return this.getProjectsCached();
31+
}
32+
33+
@cache(30_000, true, 10_000)
34+
private static async getProjectsCached(): Promise<ProjectInfo[] | undefined> {
35+
try {
36+
const result = await shellExecute(`${this.stateCommand} projects -o json`, {
37+
timeout: STATE_GENERAL_TIMEOUT,
38+
});
39+
if (!result) {
40+
return undefined;
41+
}
42+
let output = result.stdout.trimEnd();
43+
if (output[output.length - 1] === '\0') {
44+
// '\0' is a record separator.
45+
output = output.substring(0, output.length - 1);
46+
}
47+
traceVerbose(`${this.stateCommand} projects -o json: ${output}`);
48+
return JSON.parse(output);
49+
} catch (ex) {
50+
traceError(ex);
51+
return undefined;
52+
}
53+
}
54+
}

src/client/pythonEnvironments/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
import { EnvsCollectionService } from './base/locators/composite/envsCollectionService';
3737
import { IDisposable } from '../common/types';
3838
import { traceError } from '../logging';
39+
import { ActiveStateLocator } from './base/locators/lowLevel/activestateLocator';
3940

4041
/**
4142
* Set up the Python environments component (during extension activation).'
@@ -137,6 +138,7 @@ function createNonWorkspaceLocators(ext: ExtensionState): ILocator<BasicEnvInfo>
137138
// OS-independent locators go here.
138139
new PyenvLocator(),
139140
new CondaEnvironmentLocator(),
141+
new ActiveStateLocator(),
140142
new GlobalVirtualEnvironmentLocator(),
141143
new CustomVirtualEnvironmentLocator(),
142144
);

src/client/pythonEnvironments/info/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export enum EnvironmentType {
1919
MicrosoftStore = 'MicrosoftStore',
2020
Poetry = 'Poetry',
2121
VirtualEnvWrapper = 'VirtualEnvWrapper',
22+
ActiveState = 'ActiveState',
2223
Global = 'Global',
2324
System = 'System',
2425
}
@@ -30,6 +31,7 @@ export const virtualEnvTypes = [
3031
EnvironmentType.VirtualEnvWrapper,
3132
EnvironmentType.Conda,
3233
EnvironmentType.VirtualEnv,
34+
EnvironmentType.ActiveState,
3335
];
3436

3537
/**
@@ -41,6 +43,7 @@ export enum ModuleInstallerType {
4143
Pip = 'Pip',
4244
Poetry = 'Poetry',
4345
Pipenv = 'Pipenv',
46+
ActiveState = 'ActiveState',
4447
}
4548

4649
/**
@@ -114,6 +117,9 @@ export function getEnvironmentTypeName(environmentType: EnvironmentType): string
114117
case EnvironmentType.VirtualEnvWrapper: {
115118
return 'virtualenvwrapper';
116119
}
120+
case EnvironmentType.ActiveState: {
121+
return 'activestate';
122+
}
117123
default: {
118124
return '';
119125
}

src/client/pythonEnvironments/legacyIOC.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const convertedKinds = new Map(
3535
[PythonEnvKind.Poetry]: EnvironmentType.Poetry,
3636
[PythonEnvKind.Venv]: EnvironmentType.Venv,
3737
[PythonEnvKind.VirtualEnvWrapper]: EnvironmentType.VirtualEnvWrapper,
38+
[PythonEnvKind.ActiveState]: EnvironmentType.ActiveState,
3839
}),
3940
);
4041

src/test/pythonEnvironments/base/info/envKind.unit.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const KIND_NAMES: [PythonEnvKind, string][] = [
2020
[PythonEnvKind.VirtualEnvWrapper, 'virtualenvWrapper'],
2121
[PythonEnvKind.Pipenv, 'pipenv'],
2222
[PythonEnvKind.Conda, 'conda'],
23+
[PythonEnvKind.ActiveState, 'activestate'],
2324
[PythonEnvKind.OtherVirtual, 'otherVirtual'],
2425
];
2526

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import * as path from 'path';
5+
import * as sinon from 'sinon';
6+
import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info';
7+
import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies';
8+
import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils';
9+
import { ActiveStateLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/activestateLocator';
10+
import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants';
11+
import { assertBasicEnvsEqual } from '../envTestUtils';
12+
import { ExecutionResult } from '../../../../../client/common/process/types';
13+
import { createBasicEnv } from '../../common';
14+
import { getOSType, OSType } from '../../../../../client/common/utils/platform';
15+
16+
suite('ActiveState Locator', () => {
17+
const testActiveStateDir = path.join(TEST_LAYOUT_ROOT, 'activestate');
18+
let shellExecute: sinon.SinonStub;
19+
let locator: ActiveStateLocator;
20+
21+
suiteSetup(() => {
22+
locator = new ActiveStateLocator();
23+
shellExecute = sinon.stub(externalDependencies, 'shellExecute');
24+
shellExecute.callsFake((command: string) => {
25+
if (command === 'state projects -o json') {
26+
return Promise.resolve<ExecutionResult<string>>({
27+
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`,
28+
});
29+
}
30+
return Promise.reject(new Error('Command failed'));
31+
});
32+
});
33+
34+
suiteTeardown(() => sinon.restore());
35+
36+
test('iterEnvs()', async () => {
37+
const actualEnvs = await getEnvs(locator.iterEnvs());
38+
const expectedEnvs = [
39+
createBasicEnv(
40+
PythonEnvKind.ActiveState,
41+
path.join(
42+
testActiveStateDir,
43+
'c09080d1',
44+
'exec',
45+
getOSType() === OSType.Windows ? 'python3.exe' : 'python3',
46+
),
47+
),
48+
];
49+
assertBasicEnvsEqual(actualEnvs, expectedEnvs);
50+
});
51+
});

0 commit comments

Comments
 (0)