Skip to content

Commit

Permalink
Merge environment and compare environments (#14026)
Browse files Browse the repository at this point in the history
* Add some heuristic functions

* Support merging.

* Clean up identifier code

* Tweaks and fixes

* More clean up.

* versions tweak

* Address comments

* more comments

* Fix merge issues.

* Fix more merge issues.
  • Loading branch information
karthiknadig authored Sep 28, 2020
1 parent d9d4265 commit bb2ed7a
Show file tree
Hide file tree
Showing 7 changed files with 339 additions and 67 deletions.
235 changes: 235 additions & 0 deletions src/client/pythonEnvironments/base/info/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { cloneDeep } from 'lodash';
import * as path from 'path';
import {
FileInfo,
PythonDistroInfo,
PythonEnvInfo, PythonEnvKind, PythonVersion,
} from '.';
import { Architecture } from '../../../common/utils/platform';
import { arePathsSame } from '../../common/externalDependencies';
import { areEqualVersions, areEquivalentVersions } from './pythonVersion';

/**
* Checks if two environments are same.
* @param {string | PythonEnvInfo} left: environment to compare.
* @param {string | PythonEnvInfo} right: environment to compare.
* @param {boolean} allowPartialMatch: allow partial matches of properties when comparing.
*
* Remarks: The current comparison assumes that if the path to the executables are the same
* then it is the same environment. Additionally, if the paths are not same but executables
* are in the same directory and the version of python is the same than we can assume it
* to be same environment. This later case is needed for comparing windows store python,
* where multiple versions of python executables are all put in the same directory.
*/
export function areSameEnvironment(
left: string | PythonEnvInfo,
right: string | PythonEnvInfo,
allowPartialMatch?: boolean,
): boolean {
const leftFilename = typeof left === 'string' ? left : left.executable.filename;
const rightFilename = typeof right === 'string' ? right : right.executable.filename;

if (arePathsSame(leftFilename, rightFilename)) {
return true;
}

if (arePathsSame(path.dirname(leftFilename), path.dirname(rightFilename))) {
const leftVersion = typeof left === 'string' ? undefined : left.version;
const rightVersion = typeof right === 'string' ? undefined : right.version;
if (leftVersion && rightVersion) {
if (
areEqualVersions(leftVersion, rightVersion)
|| (allowPartialMatch && areEquivalentVersions(leftVersion, rightVersion))
) {
return true;
}
}
}
return false;
}

/**
* Returns a heuristic value on how much information is available in the given version object.
* @param {PythonVersion} version version object to generate heuristic from.
* @returns A heuristic value indicating the amount of info available in the object
* weighted by most important to least important fields.
* Wn > Wn-1 + Wn-2 + ... W0
*/
function getPythonVersionInfoHeuristic(version:PythonVersion): number {
let infoLevel = 0;
if (version.major > 0) {
infoLevel += 20; // W4
}

if (version.minor >= 0) {
infoLevel += 10; // W3
}

if (version.micro >= 0) {
infoLevel += 5; // W2
}

if (version.release.level) {
infoLevel += 3; // W1
}

if (version.release.serial || version.sysVersion) {
infoLevel += 1; // W0
}

return infoLevel;
}

/**
* Returns a heuristic value on how much information is available in the given executable object.
* @param {FileInfo} executable executable object to generate heuristic from.
* @returns A heuristic value indicating the amount of info available in the object
* weighted by most important to least important fields.
* Wn > Wn-1 + Wn-2 + ... W0
*/
function getFileInfoHeuristic(file:FileInfo): number {
let infoLevel = 0;
if (file.filename.length > 0) {
infoLevel += 5; // W2
}

if (file.mtime) {
infoLevel += 2; // W1
}

if (file.ctime) {
infoLevel += 1; // W0
}

return infoLevel;
}

/**
* Returns a heuristic value on how much information is available in the given distro object.
* @param {PythonDistroInfo} distro distro object to generate heuristic from.
* @returns A heuristic value indicating the amount of info available in the object
* weighted by most important to least important fields.
* Wn > Wn-1 + Wn-2 + ... W0
*/
function getDistroInfoHeuristic(distro:PythonDistroInfo):number {
let infoLevel = 0;
if (distro.org.length > 0) {
infoLevel += 20; // W3
}

if (distro.defaultDisplayName) {
infoLevel += 10; // W2
}

if (distro.binDir) {
infoLevel += 5; // W1
}

if (distro.version) {
infoLevel += 2;
}

return infoLevel;
}

/**
* Gets a prioritized list of environment types for identification.
* @returns {PythonEnvKind[]} : List of environments ordered by identification priority
*
* Remarks: This is the order of detection based on how the various distributions and tools
* configure the environment, and the fall back for identification.
* Top level we have the following environment types, since they leave a unique signature
* in the environment or * use a unique path for the environments they create.
* 1. Conda
* 2. Windows Store
* 3. PipEnv
* 4. Pyenv
* 5. Poetry
*
* Next level we have the following virtual environment tools. The are here because they
* are consumed by the tools above, and can also be used independently.
* 1. venv
* 2. virtualenvwrapper
* 3. virtualenv
*
* Last category is globally installed python, or system python.
*/
export function getPrioritizedEnvironmentKind(): PythonEnvKind[] {
return [
PythonEnvKind.CondaBase,
PythonEnvKind.Conda,
PythonEnvKind.WindowsStore,
PythonEnvKind.Pipenv,
PythonEnvKind.Pyenv,
PythonEnvKind.Poetry,
PythonEnvKind.Venv,
PythonEnvKind.VirtualEnvWrapper,
PythonEnvKind.VirtualEnv,
PythonEnvKind.OtherVirtual,
PythonEnvKind.OtherGlobal,
PythonEnvKind.MacDefault,
PythonEnvKind.System,
PythonEnvKind.Custom,
PythonEnvKind.Unknown,
];
}

/**
* Selects an environment based on the environment selection priority. This should
* match the priority in the environment identifier.
*/
export function sortEnvInfoByPriority(...envs: PythonEnvInfo[]): PythonEnvInfo[] {
// tslint:disable-next-line: no-suspicious-comment
// TODO: When we consolidate the PythonEnvKind and EnvironmentType we should have
// one location where we define priority and
const envKindByPriority:PythonEnvKind[] = getPrioritizedEnvironmentKind();
return envs.sort(
(a:PythonEnvInfo, b:PythonEnvInfo) => envKindByPriority.indexOf(a.kind) - envKindByPriority.indexOf(b.kind),
);
}

/**
* Merges properties of the `target` environment and `other` environment and returns the merged environment.
* if the value in the `target` environment is not defined or has less information. This does not mutate
* the `target` instead it returns a new object that contains the merged results.
* @param {PythonEnvInfo} target : Properties of this object are favored.
* @param {PythonEnvInfo} other : Properties of this object are used to fill the gaps in the merged result.
*/
export function mergeEnvironments(target: PythonEnvInfo, other: PythonEnvInfo): PythonEnvInfo {
const merged = cloneDeep(target);

const version = cloneDeep(
getPythonVersionInfoHeuristic(target.version) > getPythonVersionInfoHeuristic(other.version)
? target.version : other.version,
);

const executable = cloneDeep(
getFileInfoHeuristic(target.executable) > getFileInfoHeuristic(other.executable)
? target.executable : other.executable,
);
executable.sysPrefix = target.executable.sysPrefix ?? other.executable.sysPrefix;

const distro = cloneDeep(
getDistroInfoHeuristic(target.distro) > getDistroInfoHeuristic(other.distro)
? target.distro : other.distro,
);

merged.arch = merged.arch === Architecture.Unknown ? other.arch : target.arch;
merged.defaultDisplayName = merged.defaultDisplayName ?? other.defaultDisplayName;
merged.distro = distro;
merged.executable = executable;

// No need to check this just use preferred kind. Since the first thing we do is figure out the
// preferred env based on kind.
merged.kind = target.kind;

merged.location = merged.location ?? other.location;
merged.name = merged.name ?? other.name;
merged.searchLocation = merged.searchLocation ?? other.searchLocation;
merged.version = version;

return merged;
}
43 changes: 11 additions & 32 deletions src/client/pythonEnvironments/base/info/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import { Uri } from 'vscode';
import { Architecture } from '../../../common/utils/platform';
import { BasicVersionInfo, VersionInfo } from '../../../common/utils/version';
import { arePathsSame } from '../../common/externalDependencies';

/**
* IDs for the various supported Python environments.
Expand All @@ -17,26 +16,34 @@ export enum PythonEnvKind {
WindowsStore = 'global-windows-store',
Pyenv = 'global-pyenv',
CondaBase = 'global-conda-base',
Poetry = 'global-poetry',
Custom = 'global-custom',
OtherGlobal = 'global-other',
// "virtual"
Venv = 'virt-venv',
VirtualEnv = 'virt-virtualenv',
VirtualEnvWrapper = 'virt-virtualenvwrapper',
Pipenv = 'virt-pipenv',
Conda = 'virt-conda',
OtherVirtual = 'virt-other'
}

/**
* Information about a Python binary/executable.
* Information about a file.
*/
export type PythonExecutableInfo = {
export type FileInfo = {
filename: string;
sysPrefix: string;
ctime: number;
mtime: number;
};

/**
* Information about a Python binary/executable.
*/
export type PythonExecutableInfo = FileInfo & {
sysPrefix: string;
};

/**
* A (system-global) unique ID for a single Python environment.
*/
Expand Down Expand Up @@ -144,31 +151,3 @@ export type PythonEnvInfo = _PythonEnvInfo & {
defaultDisplayName?: string;
searchLocation?: Uri;
};

/**
* Determine if the given infos correspond to the same env.
*
* @param environment1 - one of the two envs to compare
* @param environment2 - one of the two envs to compare
*/
export function areSameEnvironment(
environment1: PythonEnvInfo | string,
environment2: PythonEnvInfo | string,
): boolean {
let path1: string;
let path2: string;
if (typeof environment1 === 'string') {
path1 = environment1;
} else {
path1 = environment1.executable.filename;
}
if (typeof environment2 === 'string') {
path2 = environment2;
} else {
path2 = environment2.executable.filename;
}
if (arePathsSame(path1, path2)) {
return true;
}
return false;
}
31 changes: 31 additions & 0 deletions src/client/pythonEnvironments/base/info/pythonVersion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,34 @@ export function parseVersion(versionStr: string): PythonVersion {
}
return version;
}

/**
* Checks if all the fields in the version object match.
* @param {PythonVersion} left
* @param {PythonVersion} right
* @returns {boolean}
*/
export function areEqualVersions(left: PythonVersion, right:PythonVersion): boolean {
return left === right;
}

/**
* Checks if major and minor version fields match. True here means that the python ABI is the
* same, but the micro version could be different. But for the purpose this is being used
* it does not matter.
* @param {PythonVersion} left
* @param {PythonVersion} right
* @returns {boolean}
*/
export function areEquivalentVersions(left: PythonVersion, right:PythonVersion): boolean {
if (left.major === 2 && right.major === 2) {
// We are going to assume that if the major version is 2 then the version is 2.7
return true;
}

// In the case of 3.* if major and minor match we assume that they are equivalent versions
return (
left.major === right.major
&& left.minor === right.minor
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { cloneDeep, isEqual } from 'lodash';
import { Event, EventEmitter } from 'vscode';
import { traceVerbose } from '../../../../common/logger';
import { createDeferred } from '../../../../common/utils/async';
import { areSameEnvironment, PythonEnvInfo, PythonEnvKind } from '../../info';
import { PythonEnvInfo, PythonEnvKind } from '../../info';
import { areSameEnvironment } from '../../info/env';
import {
ILocator, IPythonEnvsIterator, PythonEnvUpdatedEvent, PythonLocatorQuery,
} from '../../locator';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { cloneDeep } from 'lodash';
import { Event, EventEmitter } from 'vscode';
import { traceVerbose } from '../../../../common/logger';
import { IEnvironmentInfoService } from '../../../info/environmentInfoService';
import { areSameEnvironment, PythonEnvInfo } from '../../info';
import { PythonEnvInfo } from '../../info';
import { areSameEnvironment } from '../../info/env';
import { InterpreterInformation } from '../../info/interpreter';
import {
ILocator, IPythonEnvsIterator, PythonEnvUpdatedEvent, PythonLocatorQuery,
Expand Down
Loading

0 comments on commit bb2ed7a

Please sign in to comment.