Skip to content

Commit 4501ff5

Browse files
Add more version helpers. (#14910)
There are still some gaps in functionality related to Python versions. This change fills in those gaps and brings consistency to the existing utilities. Note that utilities for extracting Python versions from filenames (executables) will be handled in a separate change.
1 parent 7ac5036 commit 4501ff5

File tree

4 files changed

+334
-41
lines changed

4 files changed

+334
-41
lines changed

src/client/common/utils/version.ts

Lines changed: 127 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -77,27 +77,46 @@ export const EMPTY_VERSION: RawBasicVersionInfo = {
7777
micro: undefined,
7878
},
7979
};
80+
Object.freeze(EMPTY_VERSION);
81+
82+
function copyStrict<T extends BasicVersionInfo>(info: T): RawBasicVersionInfo {
83+
const copied: RawBasicVersionInfo = {
84+
major: info.major,
85+
minor: info.minor,
86+
micro: info.micro,
87+
};
88+
89+
const unnormalized = ((info as unknown) as RawBasicVersionInfo).unnormalized;
90+
if (unnormalized !== undefined) {
91+
copied.unnormalized = {
92+
major: unnormalized.major,
93+
minor: unnormalized.minor,
94+
micro: unnormalized.micro,
95+
};
96+
}
97+
98+
return copied;
99+
}
80100

81101
/**
82102
* Make a copy and set all the properties properly.
83103
*
84-
* Only the "basic" version info will be normalized. The caller
85-
* is responsible for any other properties beyond that.
104+
* Only the "basic" version info will be set (and normalized).
105+
* The caller is responsible for any other properties beyond that.
86106
*/
87107
export function normalizeBasicVersionInfo<T extends BasicVersionInfo>(info: T | undefined): T {
88108
if (!info) {
89109
return EMPTY_VERSION as T;
90110
}
91-
const norm: T = { ...info };
92-
const raw = (norm as unknown) as RawBasicVersionInfo;
111+
const norm = copyStrict(info);
93112
// Do not normalize if it has already been normalized.
94-
if (raw.unnormalized === undefined) {
95-
raw.unnormalized = {};
96-
[norm.major, raw.unnormalized.major] = normalizeVersionPart(norm.major);
97-
[norm.minor, raw.unnormalized.minor] = normalizeVersionPart(norm.minor);
98-
[norm.micro, raw.unnormalized.micro] = normalizeVersionPart(norm.micro);
113+
if (norm.unnormalized === undefined) {
114+
norm.unnormalized = {};
115+
[norm.major, norm.unnormalized.major] = normalizeVersionPart(norm.major);
116+
[norm.minor, norm.unnormalized.minor] = normalizeVersionPart(norm.minor);
117+
[norm.micro, norm.unnormalized.micro] = normalizeVersionPart(norm.micro);
99118
}
100-
return norm;
119+
return norm as T;
101120
}
102121

103122
function validateVersionPart(prop: string, part: number, unnormalized?: ErrorMsg) {
@@ -230,6 +249,55 @@ export function isVersionInfoEmpty<T extends BasicVersionInfo>(info: T): boolean
230249
return info.major < 0 && info.minor < 0 && info.micro < 0;
231250
}
232251

252+
/**
253+
* Decide if two versions are the same or if one is "less".
254+
*
255+
* Note that a less-complete object that otherwise matches
256+
* is considered "less".
257+
*
258+
* Additional checks for an otherwise "identical" version may be made
259+
* through `compareExtra()`.
260+
*
261+
* @returns - the customary comparison indicator (e.g. -1 means left is "more")
262+
* @returns - a string that indicates the property where they differ (if any)
263+
*/
264+
export function compareVersions<T extends BasicVersionInfo, V extends BasicVersionInfo>(
265+
// the versions to compare:
266+
left: T,
267+
right: V,
268+
compareExtra?: (v1: T, v2: V) => [number, string],
269+
): [number, string] {
270+
if (left.major < right.major) {
271+
return [1, 'major'];
272+
} else if (left.major > right.major) {
273+
return [-1, 'major'];
274+
} else if (left.major === -1) {
275+
// Don't bother checking minor or micro.
276+
return [0, ''];
277+
}
278+
279+
if (left.minor < right.minor) {
280+
return [1, 'minor'];
281+
} else if (left.minor > right.minor) {
282+
return [-1, 'minor'];
283+
} else if (left.minor === -1) {
284+
// Don't bother checking micro.
285+
return [0, ''];
286+
}
287+
288+
if (left.micro < right.micro) {
289+
return [1, 'micro'];
290+
} else if (left.micro > right.micro) {
291+
return [-1, 'micro'];
292+
}
293+
294+
if (compareExtra !== undefined) {
295+
return compareExtra(left, right);
296+
}
297+
298+
return [0, ''];
299+
}
300+
233301
//===========================
234302
// base version info
235303

@@ -246,15 +314,12 @@ export type VersionInfo = BasicVersionInfo & {
246314
* Make a copy and set all the properties properly.
247315
*/
248316
export function normalizeVersionInfo<T extends VersionInfo>(info: T): T {
249-
const basic = normalizeBasicVersionInfo(info);
250-
if (!info) {
251-
basic.raw = '';
252-
return basic;
253-
}
254-
const norm = { ...info, ...basic };
317+
const norm = normalizeBasicVersionInfo(info);
318+
norm.raw = info.raw;
255319
if (!norm.raw) {
256320
norm.raw = '';
257321
}
322+
// Any string value of "raw" is considered normalized.
258323
return norm;
259324
}
260325

@@ -285,6 +350,52 @@ export function parseVersionInfo<T extends VersionInfo>(verStr: string): ParseRe
285350
return result;
286351
}
287352

353+
/**
354+
* Checks if major, minor, and micro match.
355+
*
356+
* Additional checks may be made through `compareExtra()`.
357+
*/
358+
export function areIdenticalVersion<T extends BasicVersionInfo, V extends BasicVersionInfo>(
359+
// the versions to compare:
360+
left: T,
361+
right: V,
362+
compareExtra?: (v1: T, v2: V) => [number, string],
363+
): boolean {
364+
const [result] = compareVersions(left, right, compareExtra);
365+
return result === 0;
366+
}
367+
368+
/**
369+
* Checks if the versions are identical or one is more complete than other (and otherwise the same).
370+
*
371+
* At the least the major version must be set (non-negative).
372+
*/
373+
export function areSimilarVersions<T extends BasicVersionInfo, V extends BasicVersionInfo>(
374+
// the versions to compare:
375+
left: T,
376+
right: V,
377+
compareExtra?: (v1: T, v2: V) => [number, string],
378+
): boolean {
379+
const [result, prop] = compareVersions(left, right, compareExtra);
380+
if (result === 0) {
381+
return true;
382+
}
383+
384+
if (prop === 'major') {
385+
// An empty version is never similar (except to another empty version).
386+
return false;
387+
}
388+
389+
// tslint:disable:no-any
390+
if (result < 0) {
391+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
392+
return ((right as unknown) as any)[prop] === -1;
393+
}
394+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
395+
return ((left as unknown) as any)[prop] === -1;
396+
// tslint:enable:no-any
397+
}
398+
288399
//===========================
289400
// semver
290401

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { Architecture } from '../../../common/utils/platform';
88
import { arePathsSame } from '../../common/externalDependencies';
99
import { getPrioritizedEnvKinds } from './envKind';
1010
import { parseVersionFromExecutable } from './executable';
11-
import { areEqualVersions, areEquivalentVersions, isVersionEmpty } from './pythonVersion';
11+
import { areIdenticalVersion, areSimilarVersions, isVersionEmpty } from './pythonVersion';
1212

1313
import { FileInfo, PythonDistroInfo, PythonEnvInfo, PythonEnvKind, PythonReleaseLevel, PythonVersion } from '.';
1414

@@ -277,8 +277,8 @@ export function areSameEnv(
277277
const rightVersion = typeof right === 'string' ? undefined : right.version;
278278
if (leftVersion && rightVersion) {
279279
if (
280-
areEqualVersions(leftVersion, rightVersion) ||
281-
(allowPartialMatch && areEquivalentVersions(leftVersion, rightVersion))
280+
areIdenticalVersion(leftVersion, rightVersion) ||
281+
(allowPartialMatch && areSimilarVersions(leftVersion, rightVersion))
282282
) {
283283
return true;
284284
}

0 commit comments

Comments
 (0)