Skip to content

Commit

Permalink
Detect ActiveState Python runtimes (#20534)
Browse files Browse the repository at this point in the history
Closes #20532
  • Loading branch information
mitchell-as authored Feb 22, 2023
1 parent 2152cd9 commit c18e8c9
Show file tree
Hide file tree
Showing 28 changed files with 369 additions and 5 deletions.
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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%",
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions resources/report_issue_user_settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"envFile": "placeholder",
"venvPath": "placeholder",
"venvFolders": "placeholder",
"activeStateToolPath": "placeholder",
"condaPath": "placeholder",
"pipenvPath": "placeholder",
"poetryPath": "placeholder",
Expand Down
7 changes: 7 additions & 0 deletions src/client/common/configSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ export class PythonSettings implements IPythonSettings {

public venvFolders: string[] = [];

public activeStateToolPath = '';

public condaPath = '';

public pipenvPath = '';
Expand Down Expand Up @@ -254,6 +256,11 @@ export class PythonSettings implements IPythonSettings {

this.venvPath = systemVariables.resolveAny(pythonSettings.get<string>('venvPath'))!;
this.venvFolders = systemVariables.resolveAny(pythonSettings.get<string[]>('venvFolders'))!;
const activeStateToolPath = systemVariables.resolveAny(pythonSettings.get<string>('activeStateToolPath'))!;
this.activeStateToolPath =
activeStateToolPath && activeStateToolPath.length > 0
? getAbsolutePath(activeStateToolPath, workspaceRoot)
: activeStateToolPath;
const condaPath = systemVariables.resolveAny(pythonSettings.get<string>('condaPath'))!;
this.condaPath = condaPath && condaPath.length > 0 ? getAbsolutePath(condaPath, workspaceRoot) : condaPath;
const pipenvPath = systemVariables.resolveAny(pythonSettings.get<string>('pipenvPath'))!;
Expand Down
1 change: 1 addition & 0 deletions src/client/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
10 changes: 10 additions & 0 deletions src/client/interpreter/configuration/environmentTypeComparer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -93,6 +94,14 @@ export class EnvironmentTypeComparer implements IInterpreterComparer {
if (isProblematicCondaEnvironment(i)) {
return false;
}
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;
}
Expand Down Expand Up @@ -237,6 +246,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 @@ -75,6 +75,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
1 change: 1 addition & 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 Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,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 } from '../../../common/environmentManagers/activestate';

function getResolvers(): Map<PythonEnvKind, (env: BasicEnvInfo) => Promise<PythonEnvInfo>> {
const resolvers = new Map<PythonEnvKind, (_: BasicEnvInfo) => Promise<PythonEnvInfo>>();
Expand All @@ -42,6 +43,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 @@ -247,6 +249,25 @@ 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,
});
const projects = await ActiveState.getState().then((v) => v?.getProjects());
if (projects) {
for (const project of projects) {
for (const dir of project.executables) {
if (arePathsSame(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,44 @@
// 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 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;
}
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,133 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';

import * as path from 'path';
import { dirname } from 'path';
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 = {
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 {
private static statePromise: Promise<ActiveState | undefined> | undefined;

public static async getState(): Promise<ActiveState | undefined> {
if (ActiveState.statePromise === undefined) {
ActiveState.statePromise = ActiveState.locate();
}
return ActiveState.statePromise;
}

constructor() {
onDidChangePythonSetting(ACTIVESTATETOOLPATH_SETTING_KEY, () => {
ActiveState.statePromise = undefined;
});
}

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<ActiveState | undefined> {
const stateToolDir = this.getStateToolDir();
const stateCommand =
getPythonSetting<string>(ACTIVESTATETOOLPATH_SETTING_KEY) ?? ActiveState.defaultStateCommand;
if (stateToolDir && ((await pathExists(stateToolDir)) || stateCommand !== this.defaultStateCommand)) {
return new ActiveState();
}
return undefined;
}

public async getProjects(): Promise<ProjectInfo[] | undefined> {
return this.getProjectsCached();
}

private static readonly defaultStateCommand: string = 'state';

@cache(30_000, true, 10_000)
// eslint-disable-next-line class-methods-use-this
private async getProjectsCached(): Promise<ProjectInfo[] | undefined> {
try {
const stateCommand =
getPythonSetting<string>(ACTIVESTATETOOLPATH_SETTING_KEY) ?? ActiveState.defaultStateCommand;
const result = await shellExecute(`${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(`${stateCommand} projects -o editor: ${output}`);
const projects = JSON.parse(output);
ActiveState.setCachedProjectInfo(projects);
return projects;
} catch (ex) {
traceError(ex);
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;
}

private 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;
}
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
4 changes: 4 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 Down Expand Up @@ -114,6 +115,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
Loading

0 comments on commit c18e8c9

Please sign in to comment.