diff --git a/src/managers/builtin/pipListUtils.ts b/src/managers/builtin/pipListUtils.ts index 3d8d7944..e0ca55ca 100644 --- a/src/managers/builtin/pipListUtils.ts +++ b/src/managers/builtin/pipListUtils.ts @@ -4,34 +4,22 @@ export interface PipPackage { displayName: string; description: string; } -export function isValidVersion(version: string): boolean { - return /^([1-9][0-9]*!)?(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*((a|b|rc)(0|[1-9][0-9]*))?(\.post(0|[1-9][0-9]*))?(\.dev(0|[1-9][0-9]*))?$/.test( - version, - ); -} -export function parsePipList(data: string): PipPackage[] { - const collection: PipPackage[] = []; - const lines = data.split('\n').splice(2); - for (let line of lines) { - if (line.trim() === '' || line.startsWith('Package') || line.startsWith('----') || line.startsWith('[')) { - continue; - } - const parts = line.split(' ').filter((e) => e); - if (parts.length === 2) { - const name = parts[0].trim(); - const version = parts[1].trim(); - if (!isValidVersion(version)) { - continue; - } - const pkg = { - name, - version, - displayName: name, - description: version, - }; - collection.push(pkg); +export function parsePipListJson(data: string): PipPackage[] { + try { + const json = JSON.parse(data); + if (Array.isArray(json)) { + return json + .filter((item) => item.name && item.version) + .map(({ name, version }) => ({ + name, + version, + displayName: name, + description: version, + })); } + } catch (_) { + // If JSON parsing fails, return an empty array. The caller can decide how to handle this case. } - return collection; + return []; } diff --git a/src/managers/builtin/utils.ts b/src/managers/builtin/utils.ts index 7f062405..3bab6770 100644 --- a/src/managers/builtin/utils.ts +++ b/src/managers/builtin/utils.ts @@ -23,7 +23,7 @@ import { } from '../common/nativePythonFinder'; import { shortVersion, sortEnvironments } from '../common/utils'; import { runPython, runUV, shouldUseUv } from './helpers'; -import { parsePipList, PipPackage } from './pipListUtils'; +import { parsePipListJson, PipPackage } from './pipListUtils'; const PIXI_EXTENSION_ID = 'renan-r-santos.pixi-code'; const PIXI_RECOMMEND_DONT_ASK_KEY = 'pixi-extension-recommend-dont-ask'; @@ -190,7 +190,7 @@ async function refreshPipPackagesRaw(environment: PythonEnvironment, log?: LogOu const useUv = await shouldUseUv(log, environment.environmentPath.fsPath); if (useUv) { return await runUV( - ['pip', 'list', '--python', environment.execInfo.run.executable], + ['pip', 'list', '--python', environment.execInfo.run.executable, '--format=json'], undefined, log, undefined, @@ -200,7 +200,7 @@ async function refreshPipPackagesRaw(environment: PythonEnvironment, log?: LogOu try { return await runPython( environment.execInfo.run.executable, - ['-m', 'pip', 'list'], + ['-m', 'pip', 'list', '--format=json'], undefined, log, undefined, @@ -235,7 +235,7 @@ export async function refreshPipPackages( data = await refreshPipPackagesRaw(environment, log); } - return parsePipList(data); + return parsePipListJson(data); } catch (e) { log?.error('Error refreshing packages', e); showErrorMessageWithLogs(SysManagerStrings.packageRefreshError, log); diff --git a/src/test/managers/builtin/pipListUtils.unit.test.ts b/src/test/managers/builtin/pipListUtils.unit.test.ts index 24bb39df..6ba342de 100644 --- a/src/test/managers/builtin/pipListUtils.unit.test.ts +++ b/src/test/managers/builtin/pipListUtils.unit.test.ts @@ -1,22 +1,22 @@ import assert from 'assert'; import * as fs from 'fs-extra'; import * as path from 'path'; -import { parsePipList } from '../../../managers/builtin/pipListUtils'; +import { parsePipListJson } from '../../../managers/builtin/pipListUtils'; import { EXTENSION_TEST_ROOT } from '../../constants'; const TEST_DATA_ROOT = path.join(EXTENSION_TEST_ROOT, 'managers', 'builtin'); -suite('Pip List Parser tests', () => { +suite('Pip List JSON Parser tests', () => { const testNames = ['piplist1', 'piplist2', 'piplist3']; testNames.forEach((testName) => { - test(`Test parsing pip list output ${testName}`, async () => { - const pipListOutput = await fs.readFile(path.join(TEST_DATA_ROOT, `${testName}.actual.txt`), 'utf8'); + test(`Test parsing pip list JSON output ${testName}`, async () => { const expected = JSON.parse( await fs.readFile(path.join(TEST_DATA_ROOT, `${testName}.expected.json`), 'utf8'), ); + const pipListOutput = JSON.stringify(expected.packages); - const actualPackages = parsePipList(pipListOutput); + const actualPackages = parsePipListJson(pipListOutput); assert.equal(actualPackages.length, expected.packages.length, 'Unexpected number of packages'); actualPackages.forEach((actualPackage) => { @@ -34,4 +34,23 @@ suite('Pip List Parser tests', () => { }); }); }); + + test('Returns an empty array for invalid JSON input', () => { + assert.deepStrictEqual(parsePipListJson('not json'), []); + }); + + test('Skips items without a name or version', () => { + const actualPackages = parsePipListJson( + JSON.stringify([{ name: 'pip', version: '24.0' }, { name: 'setuptools' }, { version: '1.0.0' }]), + ); + + assert.deepStrictEqual(actualPackages, [ + { + name: 'pip', + version: '24.0', + displayName: 'pip', + description: '24.0', + }, + ]); + }); }); diff --git a/src/test/managers/builtin/piplist1.actual.txt b/src/test/managers/builtin/piplist1.actual.txt deleted file mode 100644 index 7d291578..00000000 --- a/src/test/managers/builtin/piplist1.actual.txt +++ /dev/null @@ -1,38 +0,0 @@ -Package Version ------------------- -------- -argcomplete 3.1.2 -black 23.12.1 -build 1.2.1 -click 8.1.7 -colorama 0.4.6 -colorlog 6.7.0 -coverage 7.6.1 -distlib 0.3.7 -exceptiongroup 1.1.3 -filelock 3.13.1 -importlib_metadata 7.1.0 -iniconfig 2.0.0 -isort 5.13.2 -mypy-extensions 1.0.0 -namedpipe 0.1.1 -nox 2024.3.2 -packaging 23.2 -pathspec 0.12.1 -pip 24.0 -pip-tools 7.4.1 -platformdirs 3.11.0 -pluggy 1.4.0 -pyproject_hooks 1.1.0 -pytest 8.1.1 -pytest-cov 5.0.0 -pywin32 306 -ruff 0.7.4 -setuptools 56.0.0 -tomli 2.0.1 -typing_extensions 4.9.0 -virtualenv 20.24.6 -wheel 0.43.0 -zipp 3.19.2 - -[notice] A new release of pip is available: 24.0 -> 24.3.1 -[notice] To update, run: python.exe -m pip install --upgrade pip \ No newline at end of file diff --git a/src/test/managers/builtin/piplist2.actual.txt b/src/test/managers/builtin/piplist2.actual.txt deleted file mode 100644 index 5cefab64..00000000 --- a/src/test/managers/builtin/piplist2.actual.txt +++ /dev/null @@ -1,35 +0,0 @@ -Package Version ------------------- -------- -argcomplete 3.1.2 -black 23.12.1 -build 1.2.1 -click 8.1.7 -colorama 0.4.6 -colorlog 6.7.0 -coverage 7.6.1 -distlib 0.3.7 -exceptiongroup 1.1.3 -filelock 3.13.1 -importlib_metadata 7.1.0 -iniconfig 2.0.0 -isort 5.13.2 -mypy-extensions 1.0.0 -namedpipe 0.1.1 -nox 2024.3.2 -packaging 23.2 -pathspec 0.12.1 -pip 24.0 -pip-tools 7.4.1 -platformdirs 3.11.0 -pluggy 1.4.0 -pyproject_hooks 1.1.0 -pytest 8.1.1 -pytest-cov 5.0.0 -pywin32 306 -ruff 0.7.4 -setuptools 56.0.0 -tomli 2.0.1 -typing_extensions 4.9.0 -virtualenv 20.24.6 -wheel 0.43.0 -zipp 3.19.2 \ No newline at end of file diff --git a/src/test/managers/builtin/piplist3.actual.txt b/src/test/managers/builtin/piplist3.actual.txt deleted file mode 100644 index 4450b42e..00000000 --- a/src/test/managers/builtin/piplist3.actual.txt +++ /dev/null @@ -1,11 +0,0 @@ -Package Version ----------- ------- -altgraph 0.17.2 -future 0.18.2 -macholib 1.15.2 -pip 21.2.4 -setuptools 58.0.4 -six 1.15.0 -wheel 0.37.0 -WARNING: You are using pip version 21.2.4; however, version 25.2 is available. -You should consider upgrading via the '/Library/Developer/CommandLineTools/usr/bin/python3 -m pip install --upgrade pip' command.