Skip to content

Commit 218dd8d

Browse files
Add PythonEnvInfo-related helpers. (#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 218dd8d

File tree

16 files changed

+1042
-336
lines changed

16 files changed

+1042
-336
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: 127 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,123 @@
33

44
import { cloneDeep } from 'lodash';
55
import * as path from 'path';
6+
import { Architecture } from '../../../common/utils/platform';
7+
import { arePathsSame } from '../../common/externalDependencies';
8+
import { areEqualVersions, areEquivalentVersions } from './pythonVersion';
9+
610
import {
711
FileInfo,
812
PythonDistroInfo,
9-
PythonEnvInfo, PythonEnvKind, PythonVersion,
13+
PythonEnvInfo,
14+
PythonEnvKind,
15+
PythonReleaseLevel,
16+
PythonVersion,
1017
} from '.';
11-
import { Architecture } from '../../../common/utils/platform';
12-
import { arePathsSame } from '../../common/externalDependencies';
13-
import { areEqualVersions, areEquivalentVersions } from './pythonVersion';
18+
19+
/**
20+
* Create a new info object with all values empty.
21+
*
22+
* @param init - if provided, these values are applied to the new object
23+
*/
24+
export function buildEnvInfo(init?: {
25+
kind?: PythonEnvKind;
26+
executable?: string;
27+
location?: string;
28+
version?: PythonVersion;
29+
}): PythonEnvInfo {
30+
const env = {
31+
kind: PythonEnvKind.Unknown,
32+
executable: {
33+
filename: '',
34+
sysPrefix: '',
35+
ctime: -1,
36+
mtime: -1,
37+
},
38+
name: '',
39+
location: '',
40+
version: {
41+
major: -1,
42+
minor: -1,
43+
micro: -1,
44+
release: {
45+
level: PythonReleaseLevel.Final,
46+
serial: 0,
47+
},
48+
},
49+
arch: Architecture.Unknown,
50+
distro: {
51+
org: '',
52+
},
53+
};
54+
if (init !== undefined) {
55+
updateEnv(env, init);
56+
}
57+
return env;
58+
}
59+
60+
/**
61+
* Return a deep copy of the given env info.
62+
*
63+
* @param updates - if provided, these values are applied to the copy
64+
*/
65+
export function copyEnvInfo(
66+
env: PythonEnvInfo,
67+
updates?: {
68+
kind?: PythonEnvKind,
69+
},
70+
): PythonEnvInfo {
71+
// We don't care whether or not extra/hidden properties
72+
// get preserved, so we do the easy thing here.
73+
const copied = cloneDeep(env);
74+
if (updates !== undefined) {
75+
updateEnv(copied, updates);
76+
}
77+
return copied;
78+
}
79+
80+
function updateEnv(env: PythonEnvInfo, updates: {
81+
kind?: PythonEnvKind;
82+
executable?: string;
83+
location?: string;
84+
version?: PythonVersion;
85+
}): void {
86+
if (updates.kind !== undefined) {
87+
env.kind = updates.kind;
88+
}
89+
if (updates.executable !== undefined) {
90+
env.executable.filename = updates.executable;
91+
}
92+
if (updates.location !== undefined) {
93+
env.location = updates.location;
94+
}
95+
if (updates.version !== undefined) {
96+
env.version = updates.version;
97+
}
98+
}
99+
100+
/**
101+
* For the given data, build a normalized partial info object.
102+
*
103+
* If insufficient data is provided to generate a minimal object, such
104+
* that it is not identifiable, then `undefined` is returned.
105+
*/
106+
export function getMinimalPartialInfo(env: string | Partial<PythonEnvInfo>): Partial<PythonEnvInfo> | undefined {
107+
if (typeof env === 'string') {
108+
if (env === '') {
109+
return undefined;
110+
}
111+
return {
112+
executable: { filename: env, sysPrefix: '', ctime: -1, mtime: -1 },
113+
};
114+
}
115+
if (env.executable === undefined) {
116+
return undefined;
117+
}
118+
if (env.executable.filename === '') {
119+
return undefined;
120+
}
121+
return env;
122+
}
14123

15124
/**
16125
* Checks if two environments are same.
@@ -24,14 +133,20 @@ import { areEqualVersions, areEquivalentVersions } from './pythonVersion';
24133
* to be same environment. This later case is needed for comparing windows store python,
25134
* where multiple versions of python executables are all put in the same directory.
26135
*/
27-
export function areSameEnvironment(
28-
left: string | PythonEnvInfo,
29-
right: string | PythonEnvInfo,
136+
export function areSameEnv(
137+
left: string | Partial<PythonEnvInfo>,
138+
right: string | Partial<PythonEnvInfo>,
30139
allowPartialMatch?: boolean,
31-
): boolean {
32-
const leftFilename = typeof left === 'string' ? left : left.executable.filename;
33-
const rightFilename = typeof right === 'string' ? right : right.executable.filename;
140+
): boolean | undefined {
141+
const leftInfo = getMinimalPartialInfo(left);
142+
const rightInfo = getMinimalPartialInfo(right);
143+
if (leftInfo === undefined || rightInfo === undefined) {
144+
return undefined;
145+
}
146+
const leftFilename = leftInfo.executable!.filename;
147+
const rightFilename = rightInfo.executable!.filename;
34148

149+
// For now we assume that matching executable means they are the same.
35150
if (arePathsSame(leftFilename, rightFilename)) {
36151
return true;
37152
}
@@ -72,11 +187,11 @@ function getPythonVersionInfoHeuristic(version:PythonVersion): number {
72187
infoLevel += 5; // W2
73188
}
74189

75-
if (version.release.level) {
190+
if (version.release?.level) {
76191
infoLevel += 3; // W1
77192
}
78193

79-
if (version.release.serial || version.sysVersion) {
194+
if (version.release?.serial || version.sysVersion) {
80195
infoLevel += 1; // W0
81196
}
82197

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

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4-
import { PythonReleaseLevel, PythonVersion } from '.';
54
import { EMPTY_VERSION, parseBasicVersionInfo } from '../../../common/utils/version';
65

6+
import { PythonReleaseLevel, PythonVersion } from '.';
7+
8+
/**
9+
* Convert the given string into the corresponding Python version object.
10+
*/
711
export function parseVersion(versionStr: string): PythonVersion {
812
const parsed = parseBasicVersionInfo<PythonVersion>(versionStr);
913
if (!parsed) {

src/client/pythonEnvironments/base/locator.ts

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,18 @@ import {
1515
* A single update to a previously provided Python env object.
1616
*/
1717
export type PythonEnvUpdatedEvent = {
18+
/**
19+
* The iteration index of The env info that was previously provided.
20+
*/
21+
index: number;
1822
/**
1923
* The env info that was previously provided.
20-
*
21-
* If the event comes from `IPythonEnvsIterator.onUpdated` then
22-
* `old` was previously yielded during iteration.
2324
*/
24-
old: PythonEnvInfo;
25+
old?: PythonEnvInfo;
2526
/**
2627
* The env info that replaces the old info.
2728
*/
28-
new: PythonEnvInfo;
29+
update: PythonEnvInfo;
2930
};
3031

3132
/**
@@ -73,23 +74,39 @@ export const NOOP_ITERATOR: IPythonEnvsIterator = iterEmpty<PythonEnvInfo>();
7374
* This is directly correlated with the `BasicPythonEnvsChangedEvent`
7475
* emitted by watchers.
7576
*
76-
* @prop kinds - if provided, results should be limited to these env kinds
77+
* @prop kinds - if provided, results should be limited to these env
78+
* kinds; if not provided, the kind of each evnironment
79+
* is not considered when filtering
7780
*/
7881
export type BasicPythonLocatorQuery = {
7982
kinds?: PythonEnvKind[];
8083
};
8184

85+
/**
86+
* The portion of a query related to env search locations.
87+
*/
88+
export type SearchLocations = {
89+
/**
90+
* The locations under which to look for environments.
91+
*/
92+
roots: Uri[];
93+
/**
94+
* If true, also look for environments that do not have a search location.
95+
*/
96+
includeNonRooted?: boolean;
97+
};
98+
8299
/**
83100
* The full set of possible info to send to a locator when requesting environments.
84101
*
85102
* This is directly correlated with the `PythonEnvsChangedEvent`
86103
* emitted by watchers.
87-
*
88-
* @prop - searchLocations - if provided, results should be limited to
89-
* within these locations
90104
*/
91105
export type PythonLocatorQuery = BasicPythonLocatorQuery & {
92-
searchLocations?: Uri[];
106+
/**
107+
* If provided, results should be limited to within these locations.
108+
*/
109+
searchLocations?: SearchLocations;
93110
};
94111

95112
type QueryForEvent<E> = E extends PythonEnvsChangedEvent ? PythonLocatorQuery : BasicPythonLocatorQuery;

0 commit comments

Comments
 (0)