Skip to content

Commit dc5ef1a

Browse files
ericsnowcurrentlyKartik Raj
authored and
Kartik Raj
committed
Add PythonEnvInfo-related helpers. (microsoft#14051)
This PR adds some basic helpers that we use in a subsequent PR. The following small drive-by changes are also included: * drop PythonEnvInfo.id property * make some internal helpers public
1 parent bb2ed7a commit dc5ef1a

File tree

16 files changed

+1204
-470
lines changed

16 files changed

+1204
-470
lines changed

src/client/common/utils/misc.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,53 @@ export function isUri(resource?: Uri | any): resource is Uri {
130130
return typeof uri.path === 'string' && typeof uri.scheme === 'string';
131131
}
132132

133+
/**
134+
* Create a filter func that determine if the given URI and candidate match.
135+
*
136+
* The scheme must match, as well as path.
137+
*
138+
* @param checkParent - if `true`, match if the candidate is rooted under `uri`
139+
* @param checkChild - if `true`, match if `uri` is rooted under the candidate
140+
* @param checkExact - if `true`, match if the candidate matches `uri` exactly
141+
*/
142+
export function getURIFilter(
143+
uri: Uri,
144+
opts: {
145+
checkParent?: boolean;
146+
checkChild?: boolean;
147+
checkExact?: boolean;
148+
} = { checkExact: true }
149+
): (u: Uri) => boolean {
150+
let uriPath = uri.path;
151+
while (uri.path.endsWith('/')) {
152+
uriPath = uriPath.slice(0, -1);
153+
}
154+
const uriRoot = `${uriPath}/`;
155+
function filter(candidate: Uri): boolean {
156+
if (candidate.scheme !== uri.scheme) {
157+
return false;
158+
}
159+
let candidatePath = candidate.path;
160+
while (candidate.path.endsWith('/')) {
161+
candidatePath = candidatePath.slice(0, -1);
162+
}
163+
if (opts.checkExact && candidatePath === uriPath) {
164+
return true;
165+
}
166+
if (opts.checkParent && candidatePath.startsWith(uriRoot)) {
167+
return true;
168+
}
169+
if (opts.checkChild) {
170+
const candidateRoot = `{candidatePath}/`;
171+
if (uriPath.startsWith(candidateRoot)) {
172+
return true;
173+
}
174+
}
175+
return false;
176+
}
177+
return filter;
178+
}
179+
133180
export function isNotebookCell(documentOrUri: TextDocument | Uri): boolean {
134181
const uri = isUri(documentOrUri) ? documentOrUri : documentOrUri.uri;
135182
return uri.scheme.includes(NotebookCellScheme);

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

Lines changed: 138 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,124 @@
44
import { cloneDeep } from 'lodash';
55
import * as path from 'path';
66
import {
7-
FileInfo,
8-
PythonDistroInfo,
9-
PythonEnvInfo, PythonEnvKind, PythonVersion,
7+
FileInfo, PythonDistroInfo, PythonEnvInfo, PythonEnvKind, PythonReleaseLevel, PythonVersion
108
} from '.';
119
import { Architecture } from '../../../common/utils/platform';
1210
import { arePathsSame } from '../../common/externalDependencies';
1311
import { areEqualVersions, areEquivalentVersions } from './pythonVersion';
1412

13+
/**
14+
* Create a new info object with all values empty.
15+
*
16+
* @param init - if provided, these values are applied to the new object
17+
*/
18+
export function buildEnvInfo(init?: {
19+
kind?: PythonEnvKind;
20+
executable?: string;
21+
location?: string;
22+
version?: PythonVersion;
23+
}): PythonEnvInfo {
24+
const env = {
25+
kind: PythonEnvKind.Unknown,
26+
executable: {
27+
filename: '',
28+
sysPrefix: '',
29+
ctime: -1,
30+
mtime: -1,
31+
},
32+
name: '',
33+
location: '',
34+
searchLocation: undefined,
35+
defaultDisplayName: undefined,
36+
version: {
37+
major: -1,
38+
minor: -1,
39+
micro: -1,
40+
release: {
41+
level: PythonReleaseLevel.Final,
42+
serial: 0,
43+
},
44+
},
45+
arch: Architecture.Unknown,
46+
distro: {
47+
org: '',
48+
},
49+
};
50+
if (init !== undefined) {
51+
updateEnv(env, init);
52+
}
53+
return env;
54+
}
55+
56+
/**
57+
* Return a deep copy of the given env info.
58+
*
59+
* @param updates - if provided, these values are applied to the copy
60+
*/
61+
export function copyEnvInfo(
62+
env: PythonEnvInfo,
63+
updates?: {
64+
kind?: PythonEnvKind;
65+
},
66+
): PythonEnvInfo {
67+
// We don't care whether or not extra/hidden properties
68+
// get preserved, so we do the easy thing here.
69+
const copied = cloneDeep(env);
70+
if (updates !== undefined) {
71+
updateEnv(copied, updates);
72+
}
73+
return copied;
74+
}
75+
76+
function updateEnv(
77+
env: PythonEnvInfo,
78+
updates: {
79+
kind?: PythonEnvKind;
80+
executable?: string;
81+
location?: string;
82+
version?: PythonVersion;
83+
},
84+
): void {
85+
if (updates.kind !== undefined) {
86+
env.kind = updates.kind;
87+
}
88+
if (updates.executable !== undefined) {
89+
env.executable.filename = updates.executable;
90+
}
91+
if (updates.location !== undefined) {
92+
env.location = updates.location;
93+
}
94+
if (updates.version !== undefined) {
95+
env.version = updates.version;
96+
}
97+
}
98+
99+
/**
100+
* For the given data, build a normalized partial info object.
101+
*
102+
* If insufficient data is provided to generate a minimal object, such
103+
* that it is not identifiable, then `undefined` is returned.
104+
*/
105+
export function getMinimalPartialInfo(env: string | Partial<PythonEnvInfo>): Partial<PythonEnvInfo> | undefined {
106+
if (typeof env === 'string') {
107+
if (env === '') {
108+
return undefined;
109+
}
110+
return {
111+
executable: {
112+
filename: env, sysPrefix: '', ctime: -1, mtime: -1,
113+
},
114+
};
115+
}
116+
if (env.executable === undefined) {
117+
return undefined;
118+
}
119+
if (env.executable.filename === '') {
120+
return undefined;
121+
}
122+
return env;
123+
}
124+
15125
/**
16126
* Checks if two environments are same.
17127
* @param {string | PythonEnvInfo} left: environment to compare.
@@ -24,14 +134,20 @@ import { areEqualVersions, areEquivalentVersions } from './pythonVersion';
24134
* to be same environment. This later case is needed for comparing windows store python,
25135
* where multiple versions of python executables are all put in the same directory.
26136
*/
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;
137+
export function areSameEnv(
138+
left: string | Partial<PythonEnvInfo>,
139+
right: string | Partial<PythonEnvInfo>,
140+
allowPartialMatch = true,
141+
): boolean | undefined {
142+
const leftInfo = getMinimalPartialInfo(left);
143+
const rightInfo = getMinimalPartialInfo(right);
144+
if (leftInfo === undefined || rightInfo === undefined) {
145+
return undefined;
146+
}
147+
const leftFilename = leftInfo.executable!.filename;
148+
const rightFilename = rightInfo.executable!.filename;
34149

150+
// For now we assume that matching executable means they are the same.
35151
if (arePathsSame(leftFilename, rightFilename)) {
36152
return true;
37153
}
@@ -58,7 +174,7 @@ export function areSameEnvironment(
58174
* weighted by most important to least important fields.
59175
* Wn > Wn-1 + Wn-2 + ... W0
60176
*/
61-
function getPythonVersionInfoHeuristic(version:PythonVersion): number {
177+
function getPythonVersionInfoHeuristic(version: PythonVersion): number {
62178
let infoLevel = 0;
63179
if (version.major > 0) {
64180
infoLevel += 20; // W4
@@ -72,11 +188,11 @@ function getPythonVersionInfoHeuristic(version:PythonVersion): number {
72188
infoLevel += 5; // W2
73189
}
74190

75-
if (version.release.level) {
191+
if (version.release?.level) {
76192
infoLevel += 3; // W1
77193
}
78194

79-
if (version.release.serial || version.sysVersion) {
195+
if (version.release?.serial || version.sysVersion) {
80196
infoLevel += 1; // W0
81197
}
82198

@@ -90,7 +206,7 @@ function getPythonVersionInfoHeuristic(version:PythonVersion): number {
90206
* weighted by most important to least important fields.
91207
* Wn > Wn-1 + Wn-2 + ... W0
92208
*/
93-
function getFileInfoHeuristic(file:FileInfo): number {
209+
function getFileInfoHeuristic(file: FileInfo): number {
94210
let infoLevel = 0;
95211
if (file.filename.length > 0) {
96212
infoLevel += 5; // W2
@@ -114,7 +230,7 @@ function getFileInfoHeuristic(file:FileInfo): number {
114230
* weighted by most important to least important fields.
115231
* Wn > Wn-1 + Wn-2 + ... W0
116232
*/
117-
function getDistroInfoHeuristic(distro:PythonDistroInfo):number {
233+
function getDistroInfoHeuristic(distro: PythonDistroInfo): number {
118234
let infoLevel = 0;
119235
if (distro.org.length > 0) {
120236
infoLevel += 20; // W3
@@ -135,62 +251,6 @@ function getDistroInfoHeuristic(distro:PythonDistroInfo):number {
135251
return infoLevel;
136252
}
137253

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-
194254
/**
195255
* Merges properties of the `target` environment and `other` environment and returns the merged environment.
196256
* if the value in the `target` environment is not defined or has less information. This does not mutate
@@ -203,18 +263,19 @@ export function mergeEnvironments(target: PythonEnvInfo, other: PythonEnvInfo):
203263

204264
const version = cloneDeep(
205265
getPythonVersionInfoHeuristic(target.version) > getPythonVersionInfoHeuristic(other.version)
206-
? target.version : other.version,
266+
? target.version
267+
: other.version,
207268
);
208269

209270
const executable = cloneDeep(
210271
getFileInfoHeuristic(target.executable) > getFileInfoHeuristic(other.executable)
211-
? target.executable : other.executable,
272+
? target.executable
273+
: other.executable,
212274
);
213275
executable.sysPrefix = target.executable.sysPrefix ?? other.executable.sysPrefix;
214276

215277
const distro = cloneDeep(
216-
getDistroInfoHeuristic(target.distro) > getDistroInfoHeuristic(other.distro)
217-
? target.distro : other.distro,
278+
getDistroInfoHeuristic(target.distro) > getDistroInfoHeuristic(other.distro) ? target.distro : other.distro,
218279
);
219280

220281
merged.arch = merged.arch === Architecture.Unknown ? other.arch : target.arch;
@@ -226,8 +287,8 @@ export function mergeEnvironments(target: PythonEnvInfo, other: PythonEnvInfo):
226287
// preferred env based on kind.
227288
merged.kind = target.kind;
228289

229-
merged.location = merged.location ?? other.location;
230-
merged.name = merged.name ?? other.name;
290+
merged.location = merged.location.length ? merged.location : other.location;
291+
merged.name = merged.name.length ? merged.name : other.name;
231292
merged.searchLocation = merged.searchLocation ?? other.searchLocation;
232293
merged.version = version;
233294

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

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ export enum PythonEnvKind {
2828
OtherVirtual = 'virt-other'
2929
}
3030

31+
/**
32+
* A (system-global) unique ID for a single Python environment.
33+
*/
34+
export type PythonEnvID = string;
35+
3136
/**
3237
* Information about a file.
3338
*/
@@ -44,11 +49,6 @@ export type PythonExecutableInfo = FileInfo & {
4449
sysPrefix: string;
4550
};
4651

47-
/**
48-
* A (system-global) unique ID for a single Python environment.
49-
*/
50-
export type PythonEnvID = string;
51-
5252
/**
5353
* The most fundamental information about a Python environment.
5454
*
@@ -63,7 +63,6 @@ export type PythonEnvID = string;
6363
* @prop location - the env's location (on disk), if relevant
6464
*/
6565
export type PythonEnvBaseInfo = {
66-
id: PythonEnvID;
6766
kind: PythonEnvKind;
6867
executable: PythonExecutableInfo;
6968
// One of (name, location) must be non-empty.
@@ -99,7 +98,7 @@ export type PythonVersionRelease = {
9998
* @prop sysVersion - the raw text from `sys.version`
10099
*/
101100
export type PythonVersion = BasicVersionInfo & {
102-
release: PythonVersionRelease;
101+
release?: PythonVersionRelease;
103102
sysVersion?: string;
104103
};
105104

0 commit comments

Comments
 (0)