Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit bb2ed7a

Browse files
authored
Merge environment and compare environments (#14026)
* 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.
1 parent d9d4265 commit bb2ed7a

File tree

7 files changed

+339
-67
lines changed

7 files changed

+339
-67
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { cloneDeep } from 'lodash';
5+
import * as path from 'path';
6+
import {
7+
FileInfo,
8+
PythonDistroInfo,
9+
PythonEnvInfo, PythonEnvKind, PythonVersion,
10+
} from '.';
11+
import { Architecture } from '../../../common/utils/platform';
12+
import { arePathsSame } from '../../common/externalDependencies';
13+
import { areEqualVersions, areEquivalentVersions } from './pythonVersion';
14+
15+
/**
16+
* Checks if two environments are same.
17+
* @param {string | PythonEnvInfo} left: environment to compare.
18+
* @param {string | PythonEnvInfo} right: environment to compare.
19+
* @param {boolean} allowPartialMatch: allow partial matches of properties when comparing.
20+
*
21+
* Remarks: The current comparison assumes that if the path to the executables are the same
22+
* then it is the same environment. Additionally, if the paths are not same but executables
23+
* are in the same directory and the version of python is the same than we can assume it
24+
* to be same environment. This later case is needed for comparing windows store python,
25+
* where multiple versions of python executables are all put in the same directory.
26+
*/
27+
export function areSameEnvironment(
28+
left: string | PythonEnvInfo,
29+
right: string | PythonEnvInfo,
30+
allowPartialMatch?: boolean,
31+
): boolean {
32+
const leftFilename = typeof left === 'string' ? left : left.executable.filename;
33+
const rightFilename = typeof right === 'string' ? right : right.executable.filename;
34+
35+
if (arePathsSame(leftFilename, rightFilename)) {
36+
return true;
37+
}
38+
39+
if (arePathsSame(path.dirname(leftFilename), path.dirname(rightFilename))) {
40+
const leftVersion = typeof left === 'string' ? undefined : left.version;
41+
const rightVersion = typeof right === 'string' ? undefined : right.version;
42+
if (leftVersion && rightVersion) {
43+
if (
44+
areEqualVersions(leftVersion, rightVersion)
45+
|| (allowPartialMatch && areEquivalentVersions(leftVersion, rightVersion))
46+
) {
47+
return true;
48+
}
49+
}
50+
}
51+
return false;
52+
}
53+
54+
/**
55+
* Returns a heuristic value on how much information is available in the given version object.
56+
* @param {PythonVersion} version version object to generate heuristic from.
57+
* @returns A heuristic value indicating the amount of info available in the object
58+
* weighted by most important to least important fields.
59+
* Wn > Wn-1 + Wn-2 + ... W0
60+
*/
61+
function getPythonVersionInfoHeuristic(version:PythonVersion): number {
62+
let infoLevel = 0;
63+
if (version.major > 0) {
64+
infoLevel += 20; // W4
65+
}
66+
67+
if (version.minor >= 0) {
68+
infoLevel += 10; // W3
69+
}
70+
71+
if (version.micro >= 0) {
72+
infoLevel += 5; // W2
73+
}
74+
75+
if (version.release.level) {
76+
infoLevel += 3; // W1
77+
}
78+
79+
if (version.release.serial || version.sysVersion) {
80+
infoLevel += 1; // W0
81+
}
82+
83+
return infoLevel;
84+
}
85+
86+
/**
87+
* Returns a heuristic value on how much information is available in the given executable object.
88+
* @param {FileInfo} executable executable object to generate heuristic from.
89+
* @returns A heuristic value indicating the amount of info available in the object
90+
* weighted by most important to least important fields.
91+
* Wn > Wn-1 + Wn-2 + ... W0
92+
*/
93+
function getFileInfoHeuristic(file:FileInfo): number {
94+
let infoLevel = 0;
95+
if (file.filename.length > 0) {
96+
infoLevel += 5; // W2
97+
}
98+
99+
if (file.mtime) {
100+
infoLevel += 2; // W1
101+
}
102+
103+
if (file.ctime) {
104+
infoLevel += 1; // W0
105+
}
106+
107+
return infoLevel;
108+
}
109+
110+
/**
111+
* Returns a heuristic value on how much information is available in the given distro object.
112+
* @param {PythonDistroInfo} distro distro object to generate heuristic from.
113+
* @returns A heuristic value indicating the amount of info available in the object
114+
* weighted by most important to least important fields.
115+
* Wn > Wn-1 + Wn-2 + ... W0
116+
*/
117+
function getDistroInfoHeuristic(distro:PythonDistroInfo):number {
118+
let infoLevel = 0;
119+
if (distro.org.length > 0) {
120+
infoLevel += 20; // W3
121+
}
122+
123+
if (distro.defaultDisplayName) {
124+
infoLevel += 10; // W2
125+
}
126+
127+
if (distro.binDir) {
128+
infoLevel += 5; // W1
129+
}
130+
131+
if (distro.version) {
132+
infoLevel += 2;
133+
}
134+
135+
return infoLevel;
136+
}
137+
138+
/**
139+
* Gets a prioritized list of environment types for identification.
140+
* @returns {PythonEnvKind[]} : List of environments ordered by identification priority
141+
*
142+
* Remarks: This is the order of detection based on how the various distributions and tools
143+
* configure the environment, and the fall back for identification.
144+
* Top level we have the following environment types, since they leave a unique signature
145+
* in the environment or * use a unique path for the environments they create.
146+
* 1. Conda
147+
* 2. Windows Store
148+
* 3. PipEnv
149+
* 4. Pyenv
150+
* 5. Poetry
151+
*
152+
* Next level we have the following virtual environment tools. The are here because they
153+
* are consumed by the tools above, and can also be used independently.
154+
* 1. venv
155+
* 2. virtualenvwrapper
156+
* 3. virtualenv
157+
*
158+
* Last category is globally installed python, or system python.
159+
*/
160+
export function getPrioritizedEnvironmentKind(): PythonEnvKind[] {
161+
return [
162+
PythonEnvKind.CondaBase,
163+
PythonEnvKind.Conda,
164+
PythonEnvKind.WindowsStore,
165+
PythonEnvKind.Pipenv,
166+
PythonEnvKind.Pyenv,
167+
PythonEnvKind.Poetry,
168+
PythonEnvKind.Venv,
169+
PythonEnvKind.VirtualEnvWrapper,
170+
PythonEnvKind.VirtualEnv,
171+
PythonEnvKind.OtherVirtual,
172+
PythonEnvKind.OtherGlobal,
173+
PythonEnvKind.MacDefault,
174+
PythonEnvKind.System,
175+
PythonEnvKind.Custom,
176+
PythonEnvKind.Unknown,
177+
];
178+
}
179+
180+
/**
181+
* Selects an environment based on the environment selection priority. This should
182+
* match the priority in the environment identifier.
183+
*/
184+
export function sortEnvInfoByPriority(...envs: PythonEnvInfo[]): PythonEnvInfo[] {
185+
// tslint:disable-next-line: no-suspicious-comment
186+
// TODO: When we consolidate the PythonEnvKind and EnvironmentType we should have
187+
// one location where we define priority and
188+
const envKindByPriority:PythonEnvKind[] = getPrioritizedEnvironmentKind();
189+
return envs.sort(
190+
(a:PythonEnvInfo, b:PythonEnvInfo) => envKindByPriority.indexOf(a.kind) - envKindByPriority.indexOf(b.kind),
191+
);
192+
}
193+
194+
/**
195+
* Merges properties of the `target` environment and `other` environment and returns the merged environment.
196+
* if the value in the `target` environment is not defined or has less information. This does not mutate
197+
* the `target` instead it returns a new object that contains the merged results.
198+
* @param {PythonEnvInfo} target : Properties of this object are favored.
199+
* @param {PythonEnvInfo} other : Properties of this object are used to fill the gaps in the merged result.
200+
*/
201+
export function mergeEnvironments(target: PythonEnvInfo, other: PythonEnvInfo): PythonEnvInfo {
202+
const merged = cloneDeep(target);
203+
204+
const version = cloneDeep(
205+
getPythonVersionInfoHeuristic(target.version) > getPythonVersionInfoHeuristic(other.version)
206+
? target.version : other.version,
207+
);
208+
209+
const executable = cloneDeep(
210+
getFileInfoHeuristic(target.executable) > getFileInfoHeuristic(other.executable)
211+
? target.executable : other.executable,
212+
);
213+
executable.sysPrefix = target.executable.sysPrefix ?? other.executable.sysPrefix;
214+
215+
const distro = cloneDeep(
216+
getDistroInfoHeuristic(target.distro) > getDistroInfoHeuristic(other.distro)
217+
? target.distro : other.distro,
218+
);
219+
220+
merged.arch = merged.arch === Architecture.Unknown ? other.arch : target.arch;
221+
merged.defaultDisplayName = merged.defaultDisplayName ?? other.defaultDisplayName;
222+
merged.distro = distro;
223+
merged.executable = executable;
224+
225+
// No need to check this just use preferred kind. Since the first thing we do is figure out the
226+
// preferred env based on kind.
227+
merged.kind = target.kind;
228+
229+
merged.location = merged.location ?? other.location;
230+
merged.name = merged.name ?? other.name;
231+
merged.searchLocation = merged.searchLocation ?? other.searchLocation;
232+
merged.version = version;
233+
234+
return merged;
235+
}

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

+11-32
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import { Uri } from 'vscode';
55
import { Architecture } from '../../../common/utils/platform';
66
import { BasicVersionInfo, VersionInfo } from '../../../common/utils/version';
7-
import { arePathsSame } from '../../common/externalDependencies';
87

98
/**
109
* IDs for the various supported Python environments.
@@ -17,26 +16,34 @@ export enum PythonEnvKind {
1716
WindowsStore = 'global-windows-store',
1817
Pyenv = 'global-pyenv',
1918
CondaBase = 'global-conda-base',
19+
Poetry = 'global-poetry',
2020
Custom = 'global-custom',
2121
OtherGlobal = 'global-other',
2222
// "virtual"
2323
Venv = 'virt-venv',
2424
VirtualEnv = 'virt-virtualenv',
25+
VirtualEnvWrapper = 'virt-virtualenvwrapper',
2526
Pipenv = 'virt-pipenv',
2627
Conda = 'virt-conda',
2728
OtherVirtual = 'virt-other'
2829
}
2930

3031
/**
31-
* Information about a Python binary/executable.
32+
* Information about a file.
3233
*/
33-
export type PythonExecutableInfo = {
34+
export type FileInfo = {
3435
filename: string;
35-
sysPrefix: string;
3636
ctime: number;
3737
mtime: number;
3838
};
3939

40+
/**
41+
* Information about a Python binary/executable.
42+
*/
43+
export type PythonExecutableInfo = FileInfo & {
44+
sysPrefix: string;
45+
};
46+
4047
/**
4148
* A (system-global) unique ID for a single Python environment.
4249
*/
@@ -144,31 +151,3 @@ export type PythonEnvInfo = _PythonEnvInfo & {
144151
defaultDisplayName?: string;
145152
searchLocation?: Uri;
146153
};
147-
148-
/**
149-
* Determine if the given infos correspond to the same env.
150-
*
151-
* @param environment1 - one of the two envs to compare
152-
* @param environment2 - one of the two envs to compare
153-
*/
154-
export function areSameEnvironment(
155-
environment1: PythonEnvInfo | string,
156-
environment2: PythonEnvInfo | string,
157-
): boolean {
158-
let path1: string;
159-
let path2: string;
160-
if (typeof environment1 === 'string') {
161-
path1 = environment1;
162-
} else {
163-
path1 = environment1.executable.filename;
164-
}
165-
if (typeof environment2 === 'string') {
166-
path2 = environment2;
167-
} else {
168-
path2 = environment2.executable.filename;
169-
}
170-
if (arePathsSame(path1, path2)) {
171-
return true;
172-
}
173-
return false;
174-
}

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

+31
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,34 @@ export function parseVersion(versionStr: string): PythonVersion {
3333
}
3434
return version;
3535
}
36+
37+
/**
38+
* Checks if all the fields in the version object match.
39+
* @param {PythonVersion} left
40+
* @param {PythonVersion} right
41+
* @returns {boolean}
42+
*/
43+
export function areEqualVersions(left: PythonVersion, right:PythonVersion): boolean {
44+
return left === right;
45+
}
46+
47+
/**
48+
* Checks if major and minor version fields match. True here means that the python ABI is the
49+
* same, but the micro version could be different. But for the purpose this is being used
50+
* it does not matter.
51+
* @param {PythonVersion} left
52+
* @param {PythonVersion} right
53+
* @returns {boolean}
54+
*/
55+
export function areEquivalentVersions(left: PythonVersion, right:PythonVersion): boolean {
56+
if (left.major === 2 && right.major === 2) {
57+
// We are going to assume that if the major version is 2 then the version is 2.7
58+
return true;
59+
}
60+
61+
// In the case of 3.* if major and minor match we assume that they are equivalent versions
62+
return (
63+
left.major === right.major
64+
&& left.minor === right.minor
65+
);
66+
}

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { cloneDeep, isEqual } from 'lodash';
55
import { Event, EventEmitter } from 'vscode';
66
import { traceVerbose } from '../../../../common/logger';
77
import { createDeferred } from '../../../../common/utils/async';
8-
import { areSameEnvironment, PythonEnvInfo, PythonEnvKind } from '../../info';
8+
import { PythonEnvInfo, PythonEnvKind } from '../../info';
9+
import { areSameEnvironment } from '../../info/env';
910
import {
1011
ILocator, IPythonEnvsIterator, PythonEnvUpdatedEvent, PythonLocatorQuery,
1112
} from '../../locator';

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { cloneDeep } from 'lodash';
55
import { Event, EventEmitter } from 'vscode';
66
import { traceVerbose } from '../../../../common/logger';
77
import { IEnvironmentInfoService } from '../../../info/environmentInfoService';
8-
import { areSameEnvironment, PythonEnvInfo } from '../../info';
8+
import { PythonEnvInfo } from '../../info';
9+
import { areSameEnvironment } from '../../info/env';
910
import { InterpreterInformation } from '../../info/interpreter';
1011
import {
1112
ILocator, IPythonEnvsIterator, PythonEnvUpdatedEvent, PythonLocatorQuery,

0 commit comments

Comments
 (0)