From 8a033b509e37c695bbc4f346832e6dfd598dc2ad Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Fri, 3 Apr 2026 09:07:41 -0700 Subject: [PATCH 1/2] Enhance sorting of installable files to prioritize shallower paths in getProjectInstallable function --- src/managers/builtin/pipUtils.ts | 11 ++++++- .../managers/builtin/pipUtils.unit.test.ts | 32 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/managers/builtin/pipUtils.ts b/src/managers/builtin/pipUtils.ts index 9fd540ab..913387e8 100644 --- a/src/managers/builtin/pipUtils.ts +++ b/src/managers/builtin/pipUtils.ts @@ -295,7 +295,16 @@ export async function getProjectInstallable( const p = api.getPythonProject(uri)?.uri.fsPath; return p && fsPaths.includes(p); }) - .sort(); + .sort((a, b) => { + // Sort by path depth (shallowest first) so top-level files like + // requirements.txt appear before deeply nested ones. + const depthA = a.fsPath.split(path.sep).length; + const depthB = b.fsPath.split(path.sep).length; + if (depthA !== depthB) { + return depthA - depthB; + } + return a.fsPath.localeCompare(b.fsPath); + }); await Promise.all( filtered.map(async (uri) => { diff --git a/src/test/managers/builtin/pipUtils.unit.test.ts b/src/test/managers/builtin/pipUtils.unit.test.ts index 5ae58126..2d5421dc 100644 --- a/src/test/managers/builtin/pipUtils.unit.test.ts +++ b/src/test/managers/builtin/pipUtils.unit.test.ts @@ -199,4 +199,36 @@ suite('Pip Utils - getProjectInstallable', () => { assert.ok(firstResult.uri, 'Should have a URI'); assert.ok(firstResult.uri.fsPath.startsWith(workspacePath), 'Should be in workspace directory'); }); + + test('should sort shallower files before deeper ones', async () => { + // Arrange: Return files at different depths, with deeper ones discovered first + findFilesStub.callsFake((pattern: string) => { + const workspacePath = Uri.file('/test/path/root').fsPath; + if (pattern === '**/*requirements*.txt') { + return Promise.resolve([ + Uri.file(path.join(workspacePath, 'deep', 'nested', 'sub', 'requirements.txt')), + Uri.file(path.join(workspacePath, 'subdir', 'dev-requirements.txt')), + ]); + } else if (pattern === '*requirements*.txt') { + return Promise.resolve([Uri.file(path.join(workspacePath, 'requirements.txt'))]); + } else if (pattern === '**/requirements/*.txt') { + return Promise.resolve([]); + } else if (pattern === '**/pyproject.toml') { + return Promise.resolve([]); + } + return Promise.resolve([]); + }); + + // Act + const workspacePath = Uri.file('/test/path/root').fsPath; + const projects = [{ name: 'workspace', uri: Uri.file(workspacePath) }]; + const result = (await getProjectInstallable(mockApi as PythonEnvironmentApi, projects)).installables; + + // Assert: root-level requirements.txt should come first + assert.strictEqual(result.length, 3); + const names = result.map((r) => r.name); + assert.strictEqual(names[0], 'requirements.txt', 'Root-level requirements.txt should be first'); + assert.strictEqual(names[1], 'dev-requirements.txt', 'One-level-deep file should be second'); + assert.strictEqual(names[2], 'requirements.txt', 'Deeply nested file should be last'); + }); }); From 459e51c3d2633b4080afe40fc3a13f8011099fd3 Mon Sep 17 00:00:00 2001 From: eleanorjboyd <26030610+eleanorjboyd@users.noreply.github.com> Date: Wed, 6 May 2026 10:44:32 -0700 Subject: [PATCH 2/2] fixes --- src/managers/builtin/pipUtils.ts | 21 +++++++++++--- .../managers/builtin/pipUtils.unit.test.ts | 29 ++++++++++--------- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/managers/builtin/pipUtils.ts b/src/managers/builtin/pipUtils.ts index 913387e8..2e7a7f85 100644 --- a/src/managers/builtin/pipUtils.ts +++ b/src/managers/builtin/pipUtils.ts @@ -290,16 +290,29 @@ export async function getProjectInstallable( const uniqueResults = Array.from(new Map(results.map((uri) => [uri.fsPath, uri])).values()); const fsPaths = projects.map((p) => p.uri.fsPath); + // Compute depth relative to the owning project root so ordering reflects + // "shallower within the project", independent of where the project lives on disk. + const depthFromProject = (uri: Uri): number => { + const projectRoot = api.getPythonProject(uri)?.uri.fsPath; + if (!projectRoot) { + return Number.MAX_SAFE_INTEGER; + } + const rel = path.relative(projectRoot, uri.fsPath); + if (!rel) { + return 0; + } + return rel.split(path.sep).filter((segment) => segment.length > 0).length; + }; const filtered = uniqueResults .filter((uri) => { const p = api.getPythonProject(uri)?.uri.fsPath; return p && fsPaths.includes(p); }) .sort((a, b) => { - // Sort by path depth (shallowest first) so top-level files like - // requirements.txt appear before deeply nested ones. - const depthA = a.fsPath.split(path.sep).length; - const depthB = b.fsPath.split(path.sep).length; + // Sort by path depth relative to the project root (shallowest first) so + // top-level files like requirements.txt appear before deeply nested ones. + const depthA = depthFromProject(a); + const depthB = depthFromProject(b); if (depthA !== depthB) { return depthA - depthB; } diff --git a/src/test/managers/builtin/pipUtils.unit.test.ts b/src/test/managers/builtin/pipUtils.unit.test.ts index 2d5421dc..f817f6c7 100644 --- a/src/test/managers/builtin/pipUtils.unit.test.ts +++ b/src/test/managers/builtin/pipUtils.unit.test.ts @@ -201,16 +201,18 @@ suite('Pip Utils - getProjectInstallable', () => { }); test('should sort shallower files before deeper ones', async () => { - // Arrange: Return files at different depths, with deeper ones discovered first + // Arrange: Use the shared workspacePath from setup() so paths are platform-safe. + const workspacePath = Uri.file('/test/path/root').fsPath; + const rootReqPath = path.join(workspacePath, 'requirements.txt'); + const subdirReqPath = path.join(workspacePath, 'subdir', 'dev-requirements.txt'); + const deepReqPath = path.join(workspacePath, 'deep', 'nested', 'sub', 'requirements.txt'); + + // Return files at different depths, with deeper ones discovered first. findFilesStub.callsFake((pattern: string) => { - const workspacePath = Uri.file('/test/path/root').fsPath; if (pattern === '**/*requirements*.txt') { - return Promise.resolve([ - Uri.file(path.join(workspacePath, 'deep', 'nested', 'sub', 'requirements.txt')), - Uri.file(path.join(workspacePath, 'subdir', 'dev-requirements.txt')), - ]); + return Promise.resolve([Uri.file(deepReqPath), Uri.file(subdirReqPath)]); } else if (pattern === '*requirements*.txt') { - return Promise.resolve([Uri.file(path.join(workspacePath, 'requirements.txt'))]); + return Promise.resolve([Uri.file(rootReqPath)]); } else if (pattern === '**/requirements/*.txt') { return Promise.resolve([]); } else if (pattern === '**/pyproject.toml') { @@ -220,15 +222,16 @@ suite('Pip Utils - getProjectInstallable', () => { }); // Act - const workspacePath = Uri.file('/test/path/root').fsPath; const projects = [{ name: 'workspace', uri: Uri.file(workspacePath) }]; const result = (await getProjectInstallable(mockApi as PythonEnvironmentApi, projects)).installables; - // Assert: root-level requirements.txt should come first + // Assert: order by fsPath so the two `requirements.txt` files are unambiguous. assert.strictEqual(result.length, 3); - const names = result.map((r) => r.name); - assert.strictEqual(names[0], 'requirements.txt', 'Root-level requirements.txt should be first'); - assert.strictEqual(names[1], 'dev-requirements.txt', 'One-level-deep file should be second'); - assert.strictEqual(names[2], 'requirements.txt', 'Deeply nested file should be last'); + const fsPaths = result.map((r) => r.uri!.fsPath); + assert.deepStrictEqual( + fsPaths, + [rootReqPath, subdirReqPath, deepReqPath], + 'Files should be ordered by depth relative to the project root', + ); }); });