Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,10 @@
"python-envs.workspaceSearchPaths": {
"type": "array",
"description": "%python-envs.workspaceSearchPaths.description%",
"default": [".venv", "*/.venv"],
"default": [
".venv",
"*/.venv"
],
"scope": "resource",
"items": {
"type": "string"
Expand Down Expand Up @@ -714,6 +717,7 @@
},
"dependencies": {
"@iarna/toml": "^2.2.5",
"@renovatebot/pep440": "^4.2.4",
"@vscode/extension-telemetry": "^0.9.7",
"@vscode/test-cli": "^0.0.10",
"dotenv": "^16.4.5",
Expand Down
9 changes: 4 additions & 5 deletions src/common/extVersion.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { compare as pep440Compare, valid as pep440Valid } from '@renovatebot/pep440';
import { PYTHON_EXTENSION_ID } from './constants';
import { getExtension } from './extension.apis';
import { traceError } from './logging';
Expand All @@ -8,11 +9,9 @@ export function ensureCorrectVersion() {
return;
}

const version = extension.packageJSON.version;
const parts = version.split('.');
const major = parseInt(parts[0]);
const minor = parseInt(parts[1]);
if (major >= 2025 || (major === 2024 && minor >= 23)) {
const version = pep440Valid(extension.packageJSON.version);
const minVersion = '2024.23.0';
if (version && pep440Compare(version, minVersion) >= 0) {
return;
}
traceError('Incompatible Python extension. Please update `ms-python.python` to version 2024.23 or later.');
Expand Down
9 changes: 2 additions & 7 deletions src/managers/builtin/pipUtils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as tomljs from '@iarna/toml';
import { valid as pep440Valid } from '@renovatebot/pep440';
import * as fse from 'fs-extra';
import * as path from 'path';
import { l10n, LogOutputChannel, ProgressLocation, QuickInputButtons, QuickPickItem, Uri, window } from 'vscode';
Expand Down Expand Up @@ -56,13 +57,7 @@ export function validatePyprojectToml(toml: PyprojectToml): string | undefined {
if (version.length === 0) {
return l10n.t('Version cannot be empty in pyproject.toml.');
}
// PEP 440 version regex. Versions must follow PEP 440 format (e.g., "1.0.0", "2.1a3").
// See https://peps.python.org/pep-0440/
// This regex is adapted from the official python 'packaging' library:
// https://github.com/pypa/packaging/blob/main/src/packaging/version.py
const versionRegex =
/^v?([0-9]+!)?([0-9]+(?:\.[0-9]+)*)(?:[-_.]?(a|b|c|rc|alpha|beta|pre|preview)[-_.]?([0-9]+)?)?(?:(?:-([0-9]+))|(?:[-_.]?(post|rev|r)[-_.]?([0-9]+)?))?(?:[-_.]?(dev)[-_.]?([0-9]+)?)?(?:\+([a-z0-9]+(?:[-_.][a-z0-9]+)*))?$/i;
if (!versionRegex.test(version)) {
if (!pep440Valid(version)) {
return l10n.t('Invalid version "{0}" in pyproject.toml.', version);
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/managers/builtin/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
NativePythonEnvironmentKind,
NativePythonFinder,
} from '../common/nativePythonFinder';
import { shortVersion, sortEnvironments } from '../common/utils';
import { shortenVersionString, sortEnvironments } from '../common/utils';
import { runPython, runUV, shouldUseUv } from './helpers';
import { parsePipListJson, PipPackage } from './pipListUtils';

Expand Down Expand Up @@ -80,7 +80,7 @@ function getKindName(kind: NativePythonEnvironmentKind | undefined): string | un
function getPythonInfo(env: NativeEnvInfo): PythonEnvironmentInfo {
if (env.executable && env.version && env.prefix) {
const kindName = getKindName(env.kind);
const sv = shortVersion(env.version);
const sv = shortenVersionString(env.version);
const name = kindName ? `Python ${sv} (${kindName})` : `Python ${sv}`;
const displayName = kindName ? `Python ${sv} (${kindName})` : `Python ${sv}`;
const shortDisplayName = kindName ? `${sv} (${kindName})` : `${sv}`;
Expand Down
4 changes: 2 additions & 2 deletions src/managers/builtin/venvManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import { showErrorMessage, showInformationMessage, withProgress } from '../../co
import { findParentIfFile } from '../../features/envCommands';
import { getProjectFsPathForScope, tryFastPathGet } from '../common/fastPath';
import { NativePythonFinder } from '../common/nativePythonFinder';
import { getLatest, shortVersion, sortEnvironments } from '../common/utils';
import { getLatest, shortenVersionString, sortEnvironments } from '../common/utils';
import { promptInstallPythonViaUv } from './uvPythonInstaller';
import {
clearVenvCache,
Expand Down Expand Up @@ -117,7 +117,7 @@ export class VenvManager implements EnvironmentManager {
description: l10n.t('Create a virtual environment in workspace root'),
detail: l10n.t(
'Uses Python version {0} and installs workspace dependencies.',
shortVersion(this.globalEnv.version),
shortenVersionString(this.globalEnv.version),
),
};
}
Expand Down
4 changes: 2 additions & 2 deletions src/managers/builtin/venvUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
NativePythonEnvironmentKind,
NativePythonFinder,
} from '../common/nativePythonFinder';
import { getShellActivationCommands, shortVersion, sortEnvironments } from '../common/utils';
import { getShellActivationCommands, shortenVersionString, sortEnvironments } from '../common/utils';
import { runPython, runUV, shouldUseUv } from './helpers';
import { getProjectInstallable, PipPackages, shouldProceedAfterPyprojectValidation } from './pipUtils';
import { resolveSystemPythonEnvironmentPath } from './utils';
Expand Down Expand Up @@ -164,7 +164,7 @@ async function getPythonInfo(env: NativeEnvInfo): Promise<PythonEnvironmentInfo>

if (env.executable && env.version && env.prefix) {
const venvName = env.name ?? getName(env.executable);
const sv = shortVersion(env.version);
const sv = shortenVersionString(env.version);
const name = `${venvName} (${sv})`;
let description = undefined;
if (env.kind === NativePythonEnvironmentKind.venvUv) {
Expand Down
86 changes: 17 additions & 69 deletions src/managers/common/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { major, minor, patch, compare as pep440Compare, valid as pep440Valid } from '@renovatebot/pep440';
import * as fs from 'fs-extra';
import path from 'path';
import { commands, ConfigurationTarget, l10n, window, workspace } from 'vscode';
Expand All @@ -21,49 +22,18 @@ export function isNumber(obj: unknown): obj is number {
return typeof obj === 'number' && !isNaN(obj);
}

export function shortVersion(version: string): string {
const pattern = /(\d)\.(\d+)(?:\.(\d+)?)?/gm;
const match = pattern.exec(version);
if (match) {
if (match[3]) {
return `${match[1]}.${match[2]}.${match[3]}`;
}
return `${match[1]}.${match[2]}.x`;
}
return version;
}

export function isGreater(a: string | undefined, b: string | undefined): boolean {
if (!a && !b) {
return false;
}
if (!a) {
return false;
}
if (!b) {
return true;
}

try {
const aParts = a.split('.');
const bParts = b.split('.');
for (let i = 0; i < aParts.length; i++) {
if (i >= bParts.length) {
return true;
}
const aPart = parseInt(aParts[i], 10);
const bPart = parseInt(bParts[i], 10);
if (aPart > bPart) {
return true;
}
if (aPart < bPart) {
return false;
}
}
} catch {
return false;
/**
* Returns a short display string: "X.Y.Z" if micro is present, otherwise "X.Y.x".
* Returns `input` unchanged if it is not a valid PEP 440 version.
*/
export function shortenVersionString(input: string): string {
if (!pep440Valid(input)) {
return input;
}
return false;
const p = patch(input);
return p !== 0 || input.split('.').length >= 3
? `${major(input)}.${minor(input)}.${p}`
: `${major(input)}.${minor(input)}.x`;
}

export function sortEnvironments(collection: PythonEnvironment[]): PythonEnvironment[] {
Expand All @@ -76,7 +46,10 @@ export function sortEnvironments(collection: PythonEnvironment[]): PythonEnviron
return -1;
}
if (a.version !== b.version) {
return isGreater(a.version, b.version) ? -1 : 1;
if (pep440Valid(a.version) && pep440Valid(b.version)) {
return pep440Compare(b.version, a.version); // descending
}
return a.version ? 1 : -1;
}
const value = a.name.localeCompare(b.name);
if (value !== 0) {
Expand All @@ -96,7 +69,7 @@ export function getLatest(collection: PythonEnvironment[]): PythonEnvironment |

let latest = candidates[0];
for (const env of candidates) {
if (isGreater(env.version, latest.version)) {
if (pep440Valid(env.version) && pep440Valid(latest.version) && pep440Compare(env.version, latest.version) > 0) {
latest = env;
}
}
Comment on lines 70 to 75
Expand All @@ -114,31 +87,6 @@ export function pathForGitBash(binPath: string): string {
return isWindows() ? binPath.replace(/\\/g, '/').replace(/^([a-zA-Z]):/, '/$1') : binPath;
}

/**
* Compares two semantic version strings. Support sonly simple 1.1.1 style versions.
* @param version1 First version
* @param version2 Second version
* @returns -1 if version1 < version2, 0 if equal, 1 if version1 > version2
*/
export function compareVersions(version1: string, version2: string): number {
const v1Parts = version1.split('.').map(Number);
const v2Parts = version2.split('.').map(Number);

for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
const v1Part = v1Parts[i] || 0;
const v2Part = v2Parts[i] || 0;

if (v1Part > v2Part) {
return 1;
}
if (v1Part < v2Part) {
return -1;
}
}

return 0;
}

function buildPwshActivationCommands(ps1Path: string): PythonCommandRunConfiguration[] {
const commands: PythonCommandRunConfiguration[] = [];
if (isWindows()) {
Expand Down
18 changes: 6 additions & 12 deletions src/managers/conda/condaStepBasedFlow.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { compare as pep440Compare, valid as pep440Valid } from '@renovatebot/pep440';
import * as fse from 'fs-extra';
import * as path from 'path';
import { l10n, LogOutputChannel, QuickInputButtons, QuickPickItem, Uri } from 'vscode';
Expand Down Expand Up @@ -113,19 +114,12 @@ async function selectPythonVersion(state: CondaCreationState): Promise<StepFunct
),
);

// Sort versions by major version (descending), ignoring minor/patch for simplicity
const parseMajorMinor = (v: string) => {
const m = v.match(/^(\\d+)(?:\\.(\\d+))?/);
return { major: m ? Number(m[1]) : 0, minor: m && m[2] ? Number(m[2]) : 0 };
};

// Sort versions descending using PEP 440 comparison
versions = versions.sort((a, b) => {
const pa = parseMajorMinor(a as string);
const pb = parseMajorMinor(b as string);
if (pa.major !== pb.major) {
return pb.major - pa.major;
} // desc by major
return pb.minor - pa.minor; // desc by minor
if (!pep440Valid(a as string) || !pep440Valid(b as string)) {
return 0;
}
return pep440Compare(b as string, a as string); // descending
});
Comment on lines +117 to 123

if (!versions || versions.length === 0) {
Expand Down
24 changes: 9 additions & 15 deletions src/managers/conda/condaUtils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { compare as pep440Compare, valid as pep440Valid } from '@renovatebot/pep440';
import * as fse from 'fs-extra';
import * as os from 'os';
import * as path from 'path';
Expand Down Expand Up @@ -53,7 +54,7 @@ import {
} from '../common/nativePythonFinder';
import { selectFromCommonPackagesToInstall } from '../common/pickers';
import { Installable } from '../common/types';
import { shortVersion, sortEnvironments } from '../common/utils';
import { shortenVersionString, sortEnvironments } from '../common/utils';
import { CondaEnvManager } from './condaEnvManager';
import { getCondaHookPs1Path, getLocalActivationScript, ShellCondaInitStatus } from './condaSourcingUtils';
import { createStepBasedCondaFlow } from './condaStepBasedFlow';
Expand Down Expand Up @@ -357,7 +358,7 @@ export async function getNamedCondaPythonInfo(
envManager: EnvironmentManager,
): Promise<PythonEnvironmentInfo> {
const { shellActivation, shellDeactivation } = await buildShellActivationMapForConda(prefix, envManager, name);
const sv = shortVersion(version);
const sv = shortenVersionString(version);

return {
name: name,
Expand Down Expand Up @@ -399,7 +400,7 @@ export async function getPrefixesCondaPythonInfo(
conda: string,
envManager: EnvironmentManager,
): Promise<PythonEnvironmentInfo> {
const sv = shortVersion(version);
const sv = shortenVersionString(version);

const { shellActivation, shellDeactivation } = await buildShellActivationMapForConda(prefix, envManager);

Expand Down Expand Up @@ -993,19 +994,12 @@ export async function pickPythonVersion(
),
);

// Sort versions by major version (descending), ignoring minor/patch for simplicity
const parseMajorMinor = (v: string) => {
const m = v.match(/^(\d+)(?:\.(\d+))?/);
return { major: m ? Number(m[1]) : 0, minor: m && m[2] ? Number(m[2]) : 0 };
};

// Sort versions descending using PEP 440 comparison
versions = versions.sort((a, b) => {
const pa = parseMajorMinor(a);
const pb = parseMajorMinor(b);
if (pa.major !== pb.major) {
return pb.major - pa.major;
} // desc by major
return pb.minor - pa.minor; // desc by minor
if (!pep440Valid(a) || !pep440Valid(b)) {
return 0;
}
return pep440Compare(b, a); // descending
});
Comment on lines +997 to 1003

if (!versions || versions.length === 0) {
Expand Down
4 changes: 2 additions & 2 deletions src/managers/pipenv/pipenvUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
NativePythonEnvironmentKind,
NativePythonFinder,
} from '../common/nativePythonFinder';
import { getShellActivationCommands, shortVersion } from '../common/utils';
import { getShellActivationCommands, shortenVersionString } from '../common/utils';

export const PIPENV_PATH_KEY = `${ENVS_EXTENSION_ID}:pipenv:PIPENV_PATH`;
export const PIPENV_WORKSPACE_KEY = `${ENVS_EXTENSION_ID}:pipenv:WORKSPACE_SELECTED`;
Expand Down Expand Up @@ -115,7 +115,7 @@ async function nativeToPythonEnv(
return undefined;
}

const sv = shortVersion(info.version);
const sv = shortenVersionString(info.version);
const folderName = path.basename(info.prefix);
const name = info.name || info.displayName || folderName;
const displayName = info.displayName || `${folderName} (${sv})`;
Expand Down
4 changes: 2 additions & 2 deletions src/managers/poetry/poetryUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
NativePythonEnvironmentKind,
NativePythonFinder,
} from '../common/nativePythonFinder';
import { getShellActivationCommands, shortVersion, sortEnvironments } from '../common/utils';
import { getShellActivationCommands, shortenVersionString, sortEnvironments } from '../common/utils';

/**
* Checks if the POETRY_VIRTUALENVS_IN_PROJECT environment variable is set to a truthy value.
Expand Down Expand Up @@ -341,7 +341,7 @@ export async function nativeToPythonEnv(
return undefined;
}

const sv = shortVersion(info.version);
const sv = shortenVersionString(info.version);
const name = info.name || info.displayName || path.basename(info.prefix);
const displayName = info.displayName || `poetry (${sv})`;

Expand Down
Loading
Loading