From a1e8fd95547ec2235f064ea99c7df2223428c49a Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Tue, 26 May 2026 16:21:34 -0700 Subject: [PATCH 1/4] Packaging: Use pip list --format=json for better parsing According to [the docs](https://pip.pypa.io/en/stable/cli/pip_list/#cmdoption-format), pip list supports a JSON format for output, which is better suited for computer parsing without having to worry about rendering edge cases or building custom parsing logic. --- src/managers/builtin/pipListUtils.ts | 42 +++++++------------ src/managers/builtin/utils.ts | 8 ++-- .../builtin/pipListUtils.unit.test.ts | 29 ++++++++++--- src/test/managers/builtin/piplist1.actual.txt | 38 ----------------- src/test/managers/builtin/piplist2.actual.txt | 35 ---------------- src/test/managers/builtin/piplist3.actual.txt | 11 ----- 6 files changed, 43 insertions(+), 120 deletions(-) delete mode 100644 src/test/managers/builtin/piplist1.actual.txt delete mode 100644 src/test/managers/builtin/piplist2.actual.txt delete mode 100644 src/test/managers/builtin/piplist3.actual.txt diff --git a/src/managers/builtin/pipListUtils.ts b/src/managers/builtin/pipListUtils.ts index 3d8d7944..6ff2a893 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((item) => ({ + name: item.name, + version: item.version, + displayName: item.name, + description: item.version, + })); } + } catch (e) { + // 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..36fda6aa 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', '--format=json', environment.execInfo.run.executable], 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. From 88d9c606e1553420b4eb3bbf4b4ee20c9d74667f Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Tue, 26 May 2026 16:26:02 -0700 Subject: [PATCH 2/4] Fix linting issues --- src/managers/builtin/pipListUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/managers/builtin/pipListUtils.ts b/src/managers/builtin/pipListUtils.ts index 6ff2a893..dc4fe070 100644 --- a/src/managers/builtin/pipListUtils.ts +++ b/src/managers/builtin/pipListUtils.ts @@ -18,7 +18,7 @@ export function parsePipListJson(data: string): PipPackage[] { description: item.version, })); } - } catch (e) { + } catch (_) { // If JSON parsing fails, return an empty array. The caller can decide how to handle this case. } return []; From 5c17f2ebec9a68cc588e7978e3528240b594a0d8 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Tue, 26 May 2026 16:31:10 -0700 Subject: [PATCH 3/4] Refactor --- src/managers/builtin/pipListUtils.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/managers/builtin/pipListUtils.ts b/src/managers/builtin/pipListUtils.ts index dc4fe070..e0ca55ca 100644 --- a/src/managers/builtin/pipListUtils.ts +++ b/src/managers/builtin/pipListUtils.ts @@ -11,11 +11,11 @@ export function parsePipListJson(data: string): PipPackage[] { if (Array.isArray(json)) { return json .filter((item) => item.name && item.version) - .map((item) => ({ - name: item.name, - version: item.version, - displayName: item.name, - description: item.version, + .map(({ name, version }) => ({ + name, + version, + displayName: name, + description: version, })); } } catch (_) { From c34f8a885b8e3b0d50ac9531ec9b6576c1d92b26 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Wed, 27 May 2026 10:28:56 -0700 Subject: [PATCH 4/4] Fix argument ordering Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/managers/builtin/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/managers/builtin/utils.ts b/src/managers/builtin/utils.ts index 36fda6aa..3bab6770 100644 --- a/src/managers/builtin/utils.ts +++ b/src/managers/builtin/utils.ts @@ -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', '--format=json', environment.execInfo.run.executable], + ['pip', 'list', '--python', environment.execInfo.run.executable, '--format=json'], undefined, log, undefined,