Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detect ActiveState Python runtimes #20534

Merged
merged 24 commits into from
Feb 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
d6ba0a7
Detect ActiveState Python runtimes (#20532)
mitchell-as Jan 19, 2023
3381fe5
Attempt to fix Windows test failure.
mitchell-as Jan 23, 2023
b768ae6
Fixed invalid JSON in unit test.
mitchell-as Jan 23, 2023
625dd7c
Reduce state command timeout.
mitchell-as Jan 23, 2023
951d522
Address some PR feedback.
mitchell-as Jan 23, 2023
b62dd06
Improve ActiveState State Tool detection.
mitchell-as Jan 23, 2023
97a0b57
Addressing more PR feedback.
mitchell-as Jan 23, 2023
819d16f
Remove unnecessary IFilesystem mocks.
mitchell-as Jan 24, 2023
1850c13
Mock up an ActiveState State Tool installation in tests.
mitchell-as Jan 24, 2023
29633fa
Recommend ActiveState Python runtimes for the workspaces they are ass…
mitchell-as Jan 25, 2023
1a0d3c4
Fixed failing unit test by checking for defined interpreter path.
mitchell-as Jan 25, 2023
1d135ae
Added preference for specifying path to ActiveState's State Tool.
mitchell-as Jan 25, 2023
31545bd
Fixed failing Windows unit test.
mitchell-as Jan 31, 2023
b219494
Merge branch 'main' into main
mitchell-as Jan 31, 2023
3ef7cd6
Merge branch 'main' into main
mitchell-as Jan 31, 2023
6309806
Do not rely on receiver to cache project info.
mitchell-as Jan 31, 2023
9b01ec0
ActiveState runtimes are not virtual environments.
mitchell-as Jan 31, 2023
8083090
Updated conditional based on PR feedback.
mitchell-as Jan 31, 2023
c0ea1be
PR feedback.
mitchell-as Feb 2, 2023
e90ae16
Revert: Added preference for specifying path to ActiveState's State T…
mitchell-as Feb 2, 2023
97f6740
Merge remote-tracking branch 'upstream/main'
mitchell-as Feb 3, 2023
b5595b2
Re-added preference for specifying path to ActiveState's State Tool.
mitchell-as Feb 7, 2023
8ade316
Merge remote-tracking branch 'upstream/main'
mitchell-as Feb 7, 2023
6eb0ac3
Fix lint error.
mitchell-as Feb 7, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -66,6 +66,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'],
karrtikr marked this conversation as resolved.
Show resolved Hide resolved
// 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[];
karrtikr marked this conversation as resolved.
Show resolved Hide resolved
};

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)
karrtikr marked this conversation as resolved.
Show resolved Hide resolved
// 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