Skip to content

Commit ea8bbce

Browse files
committed
Add normalizePackageName
1 parent 21be3e2 commit ea8bbce

5 files changed

Lines changed: 63 additions & 8 deletions

File tree

src/managers/builtin/pipPackageManager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
} from '../../api';
2222
import { updatePackagesAndNotify } from '../common/packageChanges';
2323
import { getWorkspacePackagesToInstall } from './pipUtils';
24-
import { managePackages, refreshPipDirectPackageNames, refreshPipPackages } from './utils';
24+
import { managePackages, normalizePackageName, refreshPipDirectPackageNames, refreshPipPackages } from './utils';
2525
import { VenvManager } from './venvManager';
2626

2727
export class PipPackageManager implements PackageManager, Disposable {
@@ -137,6 +137,6 @@ export class PipPackageManager implements PackageManager, Disposable {
137137

138138
async getDirectPackageNames(environment: PythonEnvironment): Promise<Set<string> | undefined> {
139139
const data = await refreshPipDirectPackageNames(environment, this.log);
140-
return data ? new Set(data) : undefined;
140+
return data ? new Set(data.map(normalizePackageName)) : undefined;
141141
}
142142
}

src/managers/builtin/utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,3 +380,7 @@ export async function resolveSystemPythonEnvironmentPath(
380380
}
381381
return undefined;
382382
}
383+
384+
export function normalizePackageName(name: string): string {
385+
return name.replace(/[-_.]+/g, '-').toLowerCase();
386+
}

src/managers/common/packageChanges.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
import { Package, PackageChangeKind, PackageManager, PythonEnvironment } from '../../api';
5+
import { normalizePackageName } from '../builtin/utils';
56

67
/**
78
* Callback invoked with the computed changes when at least one change is detected.
@@ -15,17 +16,17 @@ export type PackageChangesCallback = (changes: { kind: PackageChangeKind; pkg: P
1516
* @returns An array of changes indicating which packages were added or removed.
1617
*/
1718
export function getPackageChanges(before: Package[], after: Package[]): { kind: PackageChangeKind; pkg: Package }[] {
18-
const beforeSet = new Set(before.map(({ name, version }) => `${name}==${version}`));
19-
const afterSet = new Set(after.map(({ name, version }) => `${name}==${version}`));
19+
const beforeSet = new Set(before.map(({ name, version }) => `${normalizePackageName(name)}==${version}`));
20+
const afterSet = new Set(after.map(({ name, version }) => `${normalizePackageName(name)}==${version}`));
2021
const changes: { kind: PackageChangeKind; pkg: Package }[] = [];
2122

2223
for (const pkg of after) {
23-
if (!beforeSet.has(`${pkg.name}==${pkg.version}`)) {
24+
if (!beforeSet.has(`${normalizePackageName(pkg.name)}==${pkg.version}`)) {
2425
changes.push({ kind: PackageChangeKind.add, pkg });
2526
}
2627
}
2728
for (const pkg of before) {
28-
if (!afterSet.has(`${pkg.name}==${pkg.version}`)) {
29+
if (!afterSet.has(`${normalizePackageName(pkg.name)}==${pkg.version}`)) {
2930
changes.push({ kind: PackageChangeKind.remove, pkg });
3031
}
3132
}
@@ -55,7 +56,9 @@ export async function updatePackagesAndNotify(
5556

5657
if (afterDirectDependenciesNames && afterDirectDependenciesNames.size > 0) {
5758
for (const pkg of after) {
58-
(pkg as { isTransitive?: boolean }).isTransitive = !afterDirectDependenciesNames.has(pkg.name);
59+
(pkg as { isTransitive?: boolean }).isTransitive = !afterDirectDependenciesNames.has(
60+
normalizePackageName(pkg.name),
61+
);
5962
}
6063
}
6164

src/managers/poetry/poetryPackageManager.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
} from '../../api';
2525
import { spawnProcess } from '../../common/childProcess.apis';
2626
import { showErrorMessage, showInputBox, withProgress } from '../../common/window.apis';
27+
import { normalizePackageName } from '../builtin/utils';
2728
import { updatePackagesAndNotify } from '../common/packageChanges';
2829
import { PoetryManager } from './poetryManager';
2930
import { getPoetry } from './poetryUtils';
@@ -272,7 +273,8 @@ export class PoetryPackageManager implements PackageManager, Disposable {
272273
.split('\n')
273274
.map((line) => line.trim())
274275
.map((line) => line.match(/^([a-zA-Z0-9_-]+)/)?.[1] ?? '')
275-
.filter((name) => !!name);
276+
.filter((name) => !!name)
277+
.map(normalizePackageName);
276278
return new Set(names);
277279
} catch (err) {
278280
this.log.error(`Error fetching direct package names with Poetry: ${err}`);
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import assert from 'assert';
2+
import { normalizePackageName } from '../../../managers/builtin/utils';
3+
4+
suite('normalizePackageName', () => {
5+
test('should lowercase names', () => {
6+
assert.strictEqual(normalizePackageName('Requests'), 'requests');
7+
assert.strictEqual(normalizePackageName('NUMPY'), 'numpy');
8+
});
9+
10+
test('should replace underscores with hyphens', () => {
11+
assert.strictEqual(normalizePackageName('my_package'), 'my-package');
12+
});
13+
14+
test('should replace dots with hyphens', () => {
15+
assert.strictEqual(normalizePackageName('zope.interface'), 'zope-interface');
16+
});
17+
18+
test('should collapse consecutive separators into a single hyphen', () => {
19+
assert.strictEqual(normalizePackageName('my__package'), 'my-package');
20+
assert.strictEqual(normalizePackageName('my_-package'), 'my-package');
21+
assert.strictEqual(normalizePackageName('my_.package'), 'my-package');
22+
});
23+
24+
test('should handle mixed separators and casing', () => {
25+
assert.strictEqual(normalizePackageName('My_Package.Name'), 'my-package-name');
26+
assert.strictEqual(normalizePackageName('Foo-Bar_Baz'), 'foo-bar-baz');
27+
});
28+
29+
test('should return already-normalized names unchanged', () => {
30+
assert.strictEqual(normalizePackageName('requests'), 'requests');
31+
assert.strictEqual(normalizePackageName('my-package'), 'my-package');
32+
});
33+
34+
test('should handle single-word names', () => {
35+
assert.strictEqual(normalizePackageName('pip'), 'pip');
36+
});
37+
38+
test('should produce equal results for equivalent package names', () => {
39+
const variants = ['My_Package', 'my-package', 'my.package', 'My.Package', 'MY_PACKAGE', 'my_package'];
40+
const normalized = variants.map(normalizePackageName);
41+
assert.ok(
42+
normalized.every((n) => n === normalized[0]),
43+
`All variants should normalize to the same value, got: ${JSON.stringify(normalized)}`,
44+
);
45+
});
46+
});

0 commit comments

Comments
 (0)