Skip to content

Commit 4a0d852

Browse files
edvilmeCopilot
andauthored
Packaging: Use pip list --format=json for better parsing (#1543)
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. --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 006295a commit 4a0d852

6 files changed

Lines changed: 43 additions & 120 deletions

File tree

src/managers/builtin/pipListUtils.ts

Lines changed: 15 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,22 @@ export interface PipPackage {
44
displayName: string;
55
description: string;
66
}
7-
export function isValidVersion(version: string): boolean {
8-
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(
9-
version,
10-
);
11-
}
12-
export function parsePipList(data: string): PipPackage[] {
13-
const collection: PipPackage[] = [];
147

15-
const lines = data.split('\n').splice(2);
16-
for (let line of lines) {
17-
if (line.trim() === '' || line.startsWith('Package') || line.startsWith('----') || line.startsWith('[')) {
18-
continue;
19-
}
20-
const parts = line.split(' ').filter((e) => e);
21-
if (parts.length === 2) {
22-
const name = parts[0].trim();
23-
const version = parts[1].trim();
24-
if (!isValidVersion(version)) {
25-
continue;
26-
}
27-
const pkg = {
28-
name,
29-
version,
30-
displayName: name,
31-
description: version,
32-
};
33-
collection.push(pkg);
8+
export function parsePipListJson(data: string): PipPackage[] {
9+
try {
10+
const json = JSON.parse(data);
11+
if (Array.isArray(json)) {
12+
return json
13+
.filter((item) => item.name && item.version)
14+
.map(({ name, version }) => ({
15+
name,
16+
version,
17+
displayName: name,
18+
description: version,
19+
}));
3420
}
21+
} catch (_) {
22+
// If JSON parsing fails, return an empty array. The caller can decide how to handle this case.
3523
}
36-
return collection;
24+
return [];
3725
}

src/managers/builtin/utils.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
} from '../common/nativePythonFinder';
2424
import { shortVersion, sortEnvironments } from '../common/utils';
2525
import { runPython, runUV, shouldUseUv } from './helpers';
26-
import { parsePipList, PipPackage } from './pipListUtils';
26+
import { parsePipListJson, PipPackage } from './pipListUtils';
2727

2828
const PIXI_EXTENSION_ID = 'renan-r-santos.pixi-code';
2929
const PIXI_RECOMMEND_DONT_ASK_KEY = 'pixi-extension-recommend-dont-ask';
@@ -190,7 +190,7 @@ async function refreshPipPackagesRaw(environment: PythonEnvironment, log?: LogOu
190190
const useUv = await shouldUseUv(log, environment.environmentPath.fsPath);
191191
if (useUv) {
192192
return await runUV(
193-
['pip', 'list', '--python', environment.execInfo.run.executable],
193+
['pip', 'list', '--python', environment.execInfo.run.executable, '--format=json'],
194194
undefined,
195195
log,
196196
undefined,
@@ -200,7 +200,7 @@ async function refreshPipPackagesRaw(environment: PythonEnvironment, log?: LogOu
200200
try {
201201
return await runPython(
202202
environment.execInfo.run.executable,
203-
['-m', 'pip', 'list'],
203+
['-m', 'pip', 'list', '--format=json'],
204204
undefined,
205205
log,
206206
undefined,
@@ -235,7 +235,7 @@ export async function refreshPipPackages(
235235
data = await refreshPipPackagesRaw(environment, log);
236236
}
237237

238-
return parsePipList(data);
238+
return parsePipListJson(data);
239239
} catch (e) {
240240
log?.error('Error refreshing packages', e);
241241
showErrorMessageWithLogs(SysManagerStrings.packageRefreshError, log);

src/test/managers/builtin/pipListUtils.unit.test.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
import assert from 'assert';
22
import * as fs from 'fs-extra';
33
import * as path from 'path';
4-
import { parsePipList } from '../../../managers/builtin/pipListUtils';
4+
import { parsePipListJson } from '../../../managers/builtin/pipListUtils';
55
import { EXTENSION_TEST_ROOT } from '../../constants';
66

77
const TEST_DATA_ROOT = path.join(EXTENSION_TEST_ROOT, 'managers', 'builtin');
88

9-
suite('Pip List Parser tests', () => {
9+
suite('Pip List JSON Parser tests', () => {
1010
const testNames = ['piplist1', 'piplist2', 'piplist3'];
1111

1212
testNames.forEach((testName) => {
13-
test(`Test parsing pip list output ${testName}`, async () => {
14-
const pipListOutput = await fs.readFile(path.join(TEST_DATA_ROOT, `${testName}.actual.txt`), 'utf8');
13+
test(`Test parsing pip list JSON output ${testName}`, async () => {
1514
const expected = JSON.parse(
1615
await fs.readFile(path.join(TEST_DATA_ROOT, `${testName}.expected.json`), 'utf8'),
1716
);
17+
const pipListOutput = JSON.stringify(expected.packages);
1818

19-
const actualPackages = parsePipList(pipListOutput);
19+
const actualPackages = parsePipListJson(pipListOutput);
2020

2121
assert.equal(actualPackages.length, expected.packages.length, 'Unexpected number of packages');
2222
actualPackages.forEach((actualPackage) => {
@@ -34,4 +34,23 @@ suite('Pip List Parser tests', () => {
3434
});
3535
});
3636
});
37+
38+
test('Returns an empty array for invalid JSON input', () => {
39+
assert.deepStrictEqual(parsePipListJson('not json'), []);
40+
});
41+
42+
test('Skips items without a name or version', () => {
43+
const actualPackages = parsePipListJson(
44+
JSON.stringify([{ name: 'pip', version: '24.0' }, { name: 'setuptools' }, { version: '1.0.0' }]),
45+
);
46+
47+
assert.deepStrictEqual(actualPackages, [
48+
{
49+
name: 'pip',
50+
version: '24.0',
51+
displayName: 'pip',
52+
description: '24.0',
53+
},
54+
]);
55+
});
3756
});

src/test/managers/builtin/piplist1.actual.txt

Lines changed: 0 additions & 38 deletions
This file was deleted.

src/test/managers/builtin/piplist2.actual.txt

Lines changed: 0 additions & 35 deletions
This file was deleted.

src/test/managers/builtin/piplist3.actual.txt

Lines changed: 0 additions & 11 deletions
This file was deleted.

0 commit comments

Comments
 (0)