From 9379999e2f00566120eff1c50bf9bd0eac2ff1d8 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Mon, 14 Sep 2026 17:21:10 -0700 Subject: [PATCH 1/5] PythonProjectsImpl add discoverProjectSetupFile --- src/api.ts | 11 +++ src/features/creators/autoFindProjects.ts | 2 +- src/features/projectManager.ts | 7 +- src/internal.api.ts | 59 +++++++++++-- .../creators/newScriptProject.unit.test.ts | 31 +++---- .../features/projectSetupFile.unit.test.ts | 88 +++++++++++++++++++ src/test/features/pythonApi.unit.test.ts | 35 +++++++- 7 files changed, 197 insertions(+), 36 deletions(-) create mode 100644 src/test/features/projectSetupFile.unit.test.ts diff --git a/src/api.ts b/src/api.ts index 13504a2d3..15d737ee8 100644 --- a/src/api.ts +++ b/src/api.ts @@ -598,6 +598,11 @@ export interface PackageInfo { * Whether the package is a transitive dependency. */ readonly isTransitive?: boolean; + + /** + * Whether the package needs to be installed in the environment. Defaults to `false`. + */ + readonly needsInstallation?: boolean; } /** @@ -805,6 +810,12 @@ export interface PythonProject { * The tooltip for the Python project, which can be a string or a Markdown string. */ readonly tooltip?: string | MarkdownString; + + /** + * Finds the preferred project setup file, such as `pyproject.toml`, `setup.py`, or `requirements.txt`. + * @returns The setup file URI, or `undefined` when no supported setup file exists. + */ + discoverProjectSetupFile?(): Promise; } /** diff --git a/src/features/creators/autoFindProjects.ts b/src/features/creators/autoFindProjects.ts index 9953e8783..2c22c0a0f 100644 --- a/src/features/creators/autoFindProjects.ts +++ b/src/features/creators/autoFindProjects.ts @@ -3,10 +3,10 @@ import { Uri } from 'vscode'; import { PythonProject, PythonProjectCreator, PythonProjectCreatorOptions } from '../../api'; import { ProjectCreatorString } from '../../common/localize'; import { traceInfo } from '../../common/logging'; +import { normalizePath } from '../../common/utils/pathUtils'; import { showErrorMessage, showQuickPickWithButtons, showWarningMessage } from '../../common/window.apis'; import { findFiles } from '../../common/workspace.apis'; import { PythonProjectManager, PythonProjectsImpl } from '../../internal.api'; -import { normalizePath } from '../../common/utils/pathUtils'; function getUniqueUri(uris: Uri[]): { label: string; diff --git a/src/features/projectManager.ts b/src/features/projectManager.ts index ee7133796..4a2f20684 100644 --- a/src/features/projectManager.ts +++ b/src/features/projectManager.ts @@ -3,6 +3,7 @@ import { Disposable, EventEmitter, MarkdownString, Uri, workspace } from 'vscode import { IconPath, PythonProject } from '../api'; import { DEFAULT_ENV_MANAGER_ID, DEFAULT_PACKAGE_MANAGER_ID } from '../common/constants'; import { createSimpleDebounce } from '../common/utils/debounce'; +import { normalizePath } from '../common/utils/pathUtils'; import { getConfiguration, getWorkspaceFolders, @@ -12,7 +13,6 @@ import { onDidRenameFiles, } from '../common/workspace.apis'; import { PythonProjectManager, PythonProjectSettings, PythonProjectsImpl } from '../internal.api'; -import { normalizePath } from '../common/utils/pathUtils'; import { addPythonProjectSetting, EditProjectSettings, @@ -197,10 +197,7 @@ export class PythonProjectManagerImpl implements PythonProjectManager { return new PythonProjectsImpl(name, uri, options); } - async add( - projects: PythonProject | ProjectArray, - options?: { persistSettings?: boolean }, - ): Promise { + async add(projects: PythonProject | ProjectArray, options?: { persistSettings?: boolean }): Promise { const _projects = Array.isArray(projects) ? projects : [projects]; if (_projects.length === 0) { return; diff --git a/src/internal.api.ts b/src/internal.api.ts index c8133e959..9784d4537 100644 --- a/src/internal.api.ts +++ b/src/internal.api.ts @@ -1,5 +1,15 @@ import type { Pep440Version } from '@renovatebot/pep440'; -import { CancellationError, Disposable, Event, LogOutputChannel, MarkdownString, RelativePattern, Uri } from 'vscode'; +import * as path from 'path'; +import { + CancellationError, + Disposable, + Event, + FileType, + LogOutputChannel, + MarkdownString, + RelativePattern, + Uri, +} from 'vscode'; import { CreateEnvironmentOptions, CreateEnvironmentScope, @@ -39,6 +49,7 @@ import { StopWatch } from './common/stopWatch'; import { EventNames } from './common/telemetry/constants'; import { classifyError, isTimeoutErrorType } from './common/telemetry/errorClassifier'; import { sendTelemetryEvent } from './common/telemetry/sender'; +import { stat } from './common/workspace.fs.apis'; export type EnvironmentManagerScope = undefined | string | Uri | PythonEnvironment; export type PackageManagerScope = undefined | string | Uri | PythonEnvironment | Package; @@ -461,10 +472,7 @@ export interface PythonProjectManager extends Disposable { uri: Uri, options?: { description?: string; tooltip?: string | MarkdownString; iconPath?: IconPath }, ): PythonProject; - add( - pyWorkspace: PythonProject | PythonProject[], - options?: { persistSettings?: boolean }, - ): Promise; + add(pyWorkspace: PythonProject | PythonProject[], options?: { persistSettings?: boolean }): Promise; remove(pyWorkspace: PythonProject | PythonProject[]): void; getProjects(uris?: Uri[]): ReadonlyArray; get(uri: Uri): PythonProject | undefined; @@ -530,6 +538,7 @@ export class PythonPackageImpl implements Package { public readonly uris?: readonly Uri[]; public readonly isTransitive?: boolean; + public readonly needsInstallation: boolean; constructor( public readonly pkgId: PackageId, @@ -543,10 +552,13 @@ export class PythonPackageImpl implements Package { this.iconPath = info.iconPath; this.uris = info.uris; this.isTransitive = info.isTransitive; + this.needsInstallation = info.needsInstallation ?? false; } } export class PythonProjectsImpl implements PythonProject { + private static readonly setupFileNames = ['pyproject.toml', 'setup.py', 'requirements.txt'] as const; + name: string; uri: Uri; description?: string; @@ -564,6 +576,43 @@ export class PythonProjectsImpl implements PythonProject { this.tooltip = options?.tooltip ?? uri.fsPath; this.iconPath = options?.iconPath; } + + /** + * Finds the preferred setup file at the project root. + * @returns The setup file URI, or `undefined` when no supported setup file exists. + */ + async discoverProjectSetupFile(): Promise { + let projectType: FileType; + try { + projectType = (await stat(this.uri)).type; + } catch { + return undefined; + } + + // A project URI may point directly to a setup file instead of its parent directory. + if (projectType !== FileType.Directory) { + const fileName = path.posix.basename(this.uri.path); + return projectType === FileType.File && + PythonProjectsImpl.setupFileNames.some((candidate) => candidate === fileName) + ? this.uri + : undefined; + } + + // Search directory candidates in setup-file priority order. + for (const fileName of PythonProjectsImpl.setupFileNames) { + const candidate = this.uri.with({ path: path.posix.join(this.uri.path, fileName) }); + try { + const candidateType = (await stat(candidate)).type; + if (candidateType === FileType.File) { + return candidate; + } + } catch { + // Try the next supported setup file. + } + } + + return undefined; + } } export interface ProjectCreators extends Disposable { diff --git a/src/test/features/creators/newScriptProject.unit.test.ts b/src/test/features/creators/newScriptProject.unit.test.ts index 02f7ebb9e..b3886b824 100644 --- a/src/test/features/creators/newScriptProject.unit.test.ts +++ b/src/test/features/creators/newScriptProject.unit.test.ts @@ -1,6 +1,5 @@ import assert from 'assert'; -import fsExtra from 'fs-extra'; -import * as fs from 'fs-extra'; +import fsExtra, * as fs from 'fs-extra'; import * as os from 'os'; import * as path from 'path'; import * as sinon from 'sinon'; @@ -66,9 +65,7 @@ suite('newInlineScriptTemplate / NewScriptProject', () => { const templateFile = path.resolve( path.join(NEW_PROJECT_TEMPLATES_FOLDER, 'newInlineScriptTemplate', 'script.py'), ); - const showTextDocumentStub = sinon - .stub(windowApis, 'showTextDocument') - .resolves({} as TextEditor); + const showTextDocumentStub = sinon.stub(windowApis, 'showTextDocument').resolves({} as TextEditor); // Resolve existence by the requested path (the template exists, the new // script does not) so the fixture does not depend on probe call order. sinon.stub(fsExtra, 'pathExists').callsFake(async (checkedPath) => { @@ -147,11 +144,7 @@ suite('newInlineScriptTemplate / NewScriptProject', () => { ); } for (const validName of ['console.py', 'com10.py', 'lpt10.py']) { - assert.strictEqual( - await validateInput(validName), - null, - `${validName} should remain valid on Windows`, - ); + assert.strictEqual(await validateInput(validName), null, `${validName} should remain valid on Windows`); } return undefined; }); @@ -248,11 +241,7 @@ suite('newInlineScriptTemplate / NewScriptProject', () => { const scriptFileName = 'quick_script.py'; const rootUri = Uri.file(tmpDir); const scriptDestination = path.resolve(rootUri.fsPath, scriptFileName); - const expectedTemplatePath = path.join( - NEW_PROJECT_TEMPLATES_FOLDER, - 'newInlineScriptTemplate', - 'script.py', - ); + const expectedTemplatePath = path.join(NEW_PROJECT_TEMPLATES_FOLDER, 'newInlineScriptTemplate', 'script.py'); const addStub = sinon.stub().resolves(); const projectManager = { add: addStub } as unknown as PythonProjectManager; const creator = new NewScriptProject(projectManager); @@ -292,11 +281,7 @@ suite('newInlineScriptTemplate / NewScriptProject', () => { assert.ok( instructionsStub.calledOnceWithExactly( rootUri.fsPath, - path.join( - NEW_PROJECT_TEMPLATES_FOLDER, - 'copilot-instructions-text', - 'script-copilot-instructions.md', - ), + path.join(NEW_PROJECT_TEMPLATES_FOLDER, 'copilot-instructions-text', 'script-copilot-instructions.md'), [{ searchValue: '', replaceValue: scriptFileName }], ), 'quick create should retain Copilot-instruction handling', @@ -513,7 +498,11 @@ suite('newInlineScriptTemplate / NewScriptProject', () => { await Promise.resolve(); assert.strictEqual(createSettled, false, 'create should remain pending while project registration is pending'); - assert.strictEqual(showTextDocumentStub.called, false, 'the script must not open before registration completes'); + assert.strictEqual( + showTextDocumentStub.called, + false, + 'the script must not open before registration completes', + ); assert.strictEqual(promptStub.called, false); releaseRegistration(); diff --git a/src/test/features/projectSetupFile.unit.test.ts b/src/test/features/projectSetupFile.unit.test.ts new file mode 100644 index 000000000..ded8ab6f6 --- /dev/null +++ b/src/test/features/projectSetupFile.unit.test.ts @@ -0,0 +1,88 @@ +import assert from 'assert'; +import * as sinon from 'sinon'; +import { FileStat, FileType, Uri } from 'vscode'; +import * as workspaceFs from '../../common/workspace.fs.apis'; +import { PythonProjectsImpl } from '../../internal.api'; + +function fileStat(type: FileType): FileStat { + return { type, ctime: 0, mtime: 0, size: 0 }; +} + +suite('Project setup file discovery', () => { + teardown(() => { + sinon.restore(); + }); + + test('prefers pyproject.toml and preserves the project URI scheme', async () => { + const projectUri = Uri.parse('vscode-remote://ssh-remote+host/workspace/project'); + const project = new PythonProjectsImpl('project', projectUri); + const statStub = sinon.stub(workspaceFs, 'stat').callsFake((uri) => { + if (uri.toString() === projectUri.toString()) { + return Promise.resolve(fileStat(FileType.Directory)); + } + if (uri.path.endsWith('/pyproject.toml')) { + return Promise.resolve(fileStat(FileType.File)); + } + return Promise.reject(new Error('File not found')); + }); + + const result = await project.discoverProjectSetupFile(); + + assert.strictEqual(result?.scheme, projectUri.scheme); + assert.strictEqual(result?.authority, projectUri.authority); + assert.strictEqual(result?.path, '/workspace/project/pyproject.toml'); + assert.strictEqual(statStub.callCount, 2); + }); + + test('falls back to setup.py and requirements.txt', async () => { + const projectUri = Uri.file('/workspace/project'); + const availableFileNames = new Set(['setup.py']); + sinon.stub(workspaceFs, 'stat').callsFake((uri) => { + if (uri.toString() === projectUri.toString()) { + return Promise.resolve(fileStat(FileType.Directory)); + } + const fileName = uri.path.split('/').pop(); + return fileName && availableFileNames.has(fileName) + ? Promise.resolve(fileStat(FileType.File)) + : Promise.reject(new Error('File not found')); + }); + + const setupProject = new PythonProjectsImpl('project', projectUri); + const setupFileUri = await setupProject.discoverProjectSetupFile(); + assert.strictEqual(setupFileUri?.path.endsWith('/setup.py'), true); + + availableFileNames.clear(); + availableFileNames.add('requirements.txt'); + const requirementsProject = new PythonProjectsImpl('project', projectUri); + const requirementsFileUri = await requirementsProject.discoverProjectSetupFile(); + assert.strictEqual(requirementsFileUri?.path.endsWith('/requirements.txt'), true); + }); + + test('returns undefined when no setup file exists', async () => { + const projectUri = Uri.file('/workspace/project'); + const project = new PythonProjectsImpl('project', projectUri); + sinon.stub(workspaceFs, 'stat').callsFake((uri) => { + return uri.toString() === projectUri.toString() + ? Promise.resolve(fileStat(FileType.Directory)) + : Promise.reject(new Error('File not found')); + }); + + assert.strictEqual(await project.discoverProjectSetupFile(), undefined); + }); + + test('does not append setup paths to a standalone Python file', async () => { + const scriptUri = Uri.file('/workspace/script.py'); + const project = new PythonProjectsImpl('script.py', scriptUri); + sinon.stub(workspaceFs, 'stat').resolves(fileStat(FileType.File)); + + assert.strictEqual(await project.discoverProjectSetupFile(), undefined); + }); + + test('accepts a recognized setup file as the project URI', async () => { + const setupFileUri = Uri.file('/workspace/pyproject.toml'); + const project = new PythonProjectsImpl('pyproject.toml', setupFileUri); + sinon.stub(workspaceFs, 'stat').resolves(fileStat(FileType.File)); + + assert.strictEqual(await project.discoverProjectSetupFile(), setupFileUri); + }); +}); diff --git a/src/test/features/pythonApi.unit.test.ts b/src/test/features/pythonApi.unit.test.ts index bd464b4b0..0f607e3b6 100644 --- a/src/test/features/pythonApi.unit.test.ts +++ b/src/test/features/pythonApi.unit.test.ts @@ -4,7 +4,27 @@ import { EventEmitter, Uri } from 'vscode'; import { PythonEnvironment, PythonProject } from '../../api'; import * as managerReady from '../../features/common/managerReady'; import { PythonEnvironmentApiImpl } from '../../features/pythonApi'; -import { PythonProjectManager } from '../../internal.api'; +import { PythonPackageImpl, PythonProjectManager } from '../../internal.api'; + +suite('PythonPackageImpl', () => { + const packageId = { id: 'requests', managerId: 'test.packages:pip', environmentId: 'test-env' }; + + test('does not require installation by default', () => { + const pkg = new PythonPackageImpl(packageId, { name: 'requests', displayName: 'Requests' }); + + assert.strictEqual(pkg.needsInstallation, false); + }); + + test('preserves an explicit installation requirement', () => { + const pkg = new PythonPackageImpl(packageId, { + name: 'requests', + displayName: 'Requests', + needsInstallation: true, + }); + + assert.strictEqual(pkg.needsInstallation, true); + }); +}); suite('PythonEnvironmentApiImpl - onDidChangePythonProjects', () => { test('fires event with correct added and removed projects', () => { @@ -19,7 +39,9 @@ suite('PythonEnvironmentApiImpl - onDidChangePythonProjects', () => { const mockEnvManagers = { onDidChangeActiveEnvironment: new EventEmitter().event } as unknown as ApiArgs[0]; const mockProjectCreators = {} as unknown as ApiArgs[2]; const mockTerminalManager = {} as unknown as ApiArgs[3]; - const mockEnvVarManager = { onDidChangeEnvironmentVariables: new EventEmitter().event } as unknown as ApiArgs[4]; + const mockEnvVarManager = { + onDidChangeEnvironmentVariables: new EventEmitter().event, + } as unknown as ApiArgs[4]; const api = new PythonEnvironmentApiImpl( mockEnvManagers, @@ -40,7 +62,10 @@ suite('PythonEnvironmentApiImpl - onDidChangePythonProjects', () => { assert.ok(firedEventPayload, 'Event should have fired'); assert.strictEqual((firedEventPayload as { added: PythonProject[] }).added.length, 1); - assert.strictEqual((firedEventPayload as { added: PythonProject[] }).added[0].uri.fsPath, newProject.uri.fsPath); + assert.strictEqual( + (firedEventPayload as { added: PythonProject[] }).added[0].uri.fsPath, + newProject.uri.fsPath, + ); assert.strictEqual((firedEventPayload as { removed: PythonProject[] }).removed.length, 0); firedEventPayload = null; @@ -100,7 +125,9 @@ suite('PythonEnvironmentApiImpl - getEnvironment timeout fallback', () => { } as unknown as ApiArgs[0]; const mockProjectCreators = {} as unknown as ApiArgs[2]; const mockTerminalManager = {} as unknown as ApiArgs[3]; - const mockEnvVarManager = { onDidChangeEnvironmentVariables: new EventEmitter().event } as unknown as ApiArgs[4]; + const mockEnvVarManager = { + onDidChangeEnvironmentVariables: new EventEmitter().event, + } as unknown as ApiArgs[4]; const api = new PythonEnvironmentApiImpl( mockEnvManagers, From 3a9a7c21152c60de1a166539f21da95497e35274 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Mon, 14 Sep 2026 17:27:46 -0700 Subject: [PATCH 2/5] Show project setup file in tree view --- src/features/views/projectView.ts | 27 +++++++++++++----- src/features/views/treeViewItems.ts | 28 +++++++++++++++++-- .../features/views/treeViewItems.unit.test.ts | 22 ++++++++++++++- 3 files changed, 67 insertions(+), 10 deletions(-) diff --git a/src/features/views/projectView.ts b/src/features/views/projectView.ts index c81648c3a..a326a2ac2 100644 --- a/src/features/views/projectView.ts +++ b/src/features/views/projectView.ts @@ -22,6 +22,7 @@ import { ProjectEnvironmentInfo, ProjectItem, ProjectPackage, + ProjectSetupFile, ProjectTreeItem, ProjectTreeItemKind, } from './treeViewItems'; @@ -190,8 +191,16 @@ export class ProjectView implements TreeDataProvider { if (element.kind === ProjectTreeItemKind.project) { const projectItem = element as ProjectItem; + const views: ProjectTreeItem[] = []; + if (projectItem instanceof ProjectItem) { + const setupFileUri = await projectItem.project.discoverProjectSetupFile?.(); + if (setupFileUri) { + views.push(new ProjectSetupFile(projectItem, setupFileUri)); + } + } + if (this.envManagers.managers.length === 0) { - return [ + views.push( new NoProjectEnvironment( projectItem.project, projectItem, @@ -200,35 +209,39 @@ export class ProjectView implements TreeDataProvider { undefined, '$(loading~spin)', ), - ]; + ); + return views; } const uri = projectItem.id === 'global' ? undefined : projectItem.project.uri; const manager = this.envManagers.getEnvironmentManager(uri); if (!manager) { - return [ + views.push( new NoProjectEnvironment( projectItem.project, projectItem, ProjectViews.noEnvironmentManager, ProjectViews.noEnvironmentManagerDescription, ), - ]; + ); + return views; } const environment = await this.envManagers.getEnvironment(uri); if (!environment) { - return [ + views.push( new NoProjectEnvironment( projectItem.project, projectItem, `${ProjectViews.noEnvironmentProvided} ${manager.displayName}`, ), - ]; + ); + return views; } const view = new ProjectEnvironment(projectItem, environment); this.revealMap.set(uri ? uri.fsPath : 'global', view); - return [view]; + views.push(view); + return views; } if (element.kind === ProjectTreeItemKind.environment) { diff --git a/src/features/views/treeViewItems.ts b/src/features/views/treeViewItems.ts index 84c088a32..9ec198fd4 100644 --- a/src/features/views/treeViewItems.ts +++ b/src/features/views/treeViewItems.ts @@ -1,4 +1,4 @@ -import { Command, MarkdownString, ThemeIcon, TreeItem, TreeItemCollapsibleState, l10n } from 'vscode'; +import { Command, MarkdownString, ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri, l10n } from 'vscode'; import { EnvironmentGroupInfo, IconPath, Package, PythonEnvironment, PythonProject } from '../../api'; import { INLINE_SCRIPT_MANAGER_ID } from '../../common/constants'; import { EnvViewStrings, UvInstallStrings, VenvManagerStrings } from '../../common/localize'; @@ -239,7 +239,9 @@ export class PackageTreeItem implements EnvTreeItem { item.contextValue = getPackageContextValue(pkg, parent.environment); item.description = (pkg.isTransitive ? l10n.t('(transitive) ') : '') + (pkg.description ?? pkg.version ?? ''); item.tooltip = pkg.isTransitive - ? l10n.t('This package is a dependency of another installed package. It may also have been explicitly installed.') + ? l10n.t( + 'This package is a dependency of another installed package. It may also have been explicitly installed.', + ) : pkg.tooltip; this.treeItem = item; } @@ -289,6 +291,7 @@ export class PackageRootInfoTreeItem implements EnvTreeItem { export enum ProjectTreeItemKind { project = 'project', + setupFile = 'project-setup-file', environment = 'project-environment', none = 'project-no-environment', environmentInfo = 'environment-info', @@ -323,6 +326,27 @@ export class ProjectItem implements ProjectTreeItem { } } +export class ProjectSetupFile implements ProjectTreeItem { + public readonly kind = ProjectTreeItemKind.setupFile; + public readonly id: string; + public readonly treeItem: TreeItem; + + constructor( + public readonly parent: ProjectItem, + public readonly uri: Uri, + ) { + this.id = `${parent.id}>>>setup-file`; + const item = new TreeItem(uri, TreeItemCollapsibleState.None); + item.contextValue = 'project-setup-file'; + item.command = { + command: 'vscode.open', + title: l10n.t('Open Setup File'), + arguments: [uri], + }; + this.treeItem = item; + } +} + export class GlobalProjectItem implements ProjectTreeItem { public readonly kind = ProjectTreeItemKind.project; public readonly parent: undefined; diff --git a/src/test/features/views/treeViewItems.unit.test.ts b/src/test/features/views/treeViewItems.unit.test.ts index 75e53a8a1..44fc000b7 100644 --- a/src/test/features/views/treeViewItems.unit.test.ts +++ b/src/test/features/views/treeViewItems.unit.test.ts @@ -8,7 +8,9 @@ import { NoPythonEnvTreeItem, PackageTreeItem, ProjectEnvironment, + ProjectItem, ProjectPackage, + ProjectSetupFile, PythonEnvTreeItem, PythonGroupEnvTreeItem, } from '../../../features/views/treeViewItems'; @@ -79,6 +81,20 @@ function createMockManager( } suite('Test TreeView Items', () => { + suite('ProjectSetupFile', () => { + test('opens the setup file', () => { + const parent = new ProjectItem({ name: 'project', uri: Uri.file('.') }); + const setupFileUri = Uri.file('pyproject.toml'); + + const item = new ProjectSetupFile(parent, setupFileUri); + + assert.strictEqual(item.parent, parent); + assert.strictEqual(item.treeItem.resourceUri, setupFileUri); + assert.strictEqual(item.treeItem.command?.command, 'vscode.open'); + assert.deepStrictEqual(item.treeItem.command?.arguments, [setupFileUri]); + }); + }); + suite('EnvManagerTreeItem', () => { test('Sets id to manager id for tree item identification', () => { // Arrange @@ -587,7 +603,11 @@ suite('Test TreeView Items', () => { test('Prefers package-provided iconPath over default icon', () => { // Arrange - const pkg = createMockPackage({ name: 'numpy', isTransitive: true, iconPath: new ThemeIcon('symbol-numeric') }); + const pkg = createMockPackage({ + name: 'numpy', + isTransitive: true, + iconPath: new ThemeIcon('symbol-numeric'), + }); // Act const item = new ProjectPackage(parent, pkg, manager); From b9ebfd156a42b6d970de23d2fba60372cda14190 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Mon, 14 Sep 2026 17:32:06 -0700 Subject: [PATCH 3/5] Remove unrelated changes --- src/api.ts | 5 ----- src/internal.api.ts | 2 -- src/test/features/pythonApi.unit.test.ts | 22 +--------------------- 3 files changed, 1 insertion(+), 28 deletions(-) diff --git a/src/api.ts b/src/api.ts index 15d737ee8..0f243c631 100644 --- a/src/api.ts +++ b/src/api.ts @@ -598,11 +598,6 @@ export interface PackageInfo { * Whether the package is a transitive dependency. */ readonly isTransitive?: boolean; - - /** - * Whether the package needs to be installed in the environment. Defaults to `false`. - */ - readonly needsInstallation?: boolean; } /** diff --git a/src/internal.api.ts b/src/internal.api.ts index 9784d4537..d0bcc26a6 100644 --- a/src/internal.api.ts +++ b/src/internal.api.ts @@ -538,7 +538,6 @@ export class PythonPackageImpl implements Package { public readonly uris?: readonly Uri[]; public readonly isTransitive?: boolean; - public readonly needsInstallation: boolean; constructor( public readonly pkgId: PackageId, @@ -552,7 +551,6 @@ export class PythonPackageImpl implements Package { this.iconPath = info.iconPath; this.uris = info.uris; this.isTransitive = info.isTransitive; - this.needsInstallation = info.needsInstallation ?? false; } } diff --git a/src/test/features/pythonApi.unit.test.ts b/src/test/features/pythonApi.unit.test.ts index 0f607e3b6..7246dda03 100644 --- a/src/test/features/pythonApi.unit.test.ts +++ b/src/test/features/pythonApi.unit.test.ts @@ -4,27 +4,7 @@ import { EventEmitter, Uri } from 'vscode'; import { PythonEnvironment, PythonProject } from '../../api'; import * as managerReady from '../../features/common/managerReady'; import { PythonEnvironmentApiImpl } from '../../features/pythonApi'; -import { PythonPackageImpl, PythonProjectManager } from '../../internal.api'; - -suite('PythonPackageImpl', () => { - const packageId = { id: 'requests', managerId: 'test.packages:pip', environmentId: 'test-env' }; - - test('does not require installation by default', () => { - const pkg = new PythonPackageImpl(packageId, { name: 'requests', displayName: 'Requests' }); - - assert.strictEqual(pkg.needsInstallation, false); - }); - - test('preserves an explicit installation requirement', () => { - const pkg = new PythonPackageImpl(packageId, { - name: 'requests', - displayName: 'Requests', - needsInstallation: true, - }); - - assert.strictEqual(pkg.needsInstallation, true); - }); -}); +import { PythonProjectManager } from '../../internal.api'; suite('PythonEnvironmentApiImpl - onDidChangePythonProjects', () => { test('fires event with correct added and removed projects', () => { From a2409d14a8eb420bf420ea3e21a0c0bc90148857 Mon Sep 17 00:00:00 2001 From: Eduardo Villalpando Mello Date: Tue, 15 Sep 2026 10:28:04 -0700 Subject: [PATCH 4/5] Adjust naming --- src/api.ts | 7 +-- src/features/views/projectView.ts | 8 +-- src/features/views/treeViewItems.ts | 12 ++--- src/internal.api.ts | 23 ++++---- ...ts => projectDependencyFiles.unit.test.ts} | 52 +++++++++++-------- .../features/views/treeViewItems.unit.test.ts | 14 ++--- 6 files changed, 64 insertions(+), 52 deletions(-) rename src/test/features/{projectSetupFile.unit.test.ts => projectDependencyFiles.unit.test.ts} (56%) diff --git a/src/api.ts b/src/api.ts index 0f243c631..c9dc53aa2 100644 --- a/src/api.ts +++ b/src/api.ts @@ -807,10 +807,11 @@ export interface PythonProject { readonly tooltip?: string | MarkdownString; /** - * Finds the preferred project setup file, such as `pyproject.toml`, `setup.py`, or `requirements.txt`. - * @returns The setup file URI, or `undefined` when no supported setup file exists. + * Finds the preferred dependency file, such as `requirements.txt`, `pyproject.toml`, + * `requirements.in`, or `environment.yml`. + * @returns The dependency file URI, or `undefined` when no supported dependency file exists. */ - discoverProjectSetupFile?(): Promise; + discoverDependencyFiles?(): Promise; } /** diff --git a/src/features/views/projectView.ts b/src/features/views/projectView.ts index a326a2ac2..ce7ad5593 100644 --- a/src/features/views/projectView.ts +++ b/src/features/views/projectView.ts @@ -18,11 +18,11 @@ import { ITemporaryStateManager } from './temporaryStateManager'; import { GlobalProjectItem, NoProjectEnvironment, + ProjectDependencyFile, ProjectEnvironment, ProjectEnvironmentInfo, ProjectItem, ProjectPackage, - ProjectSetupFile, ProjectTreeItem, ProjectTreeItemKind, } from './treeViewItems'; @@ -193,9 +193,9 @@ export class ProjectView implements TreeDataProvider { const projectItem = element as ProjectItem; const views: ProjectTreeItem[] = []; if (projectItem instanceof ProjectItem) { - const setupFileUri = await projectItem.project.discoverProjectSetupFile?.(); - if (setupFileUri) { - views.push(new ProjectSetupFile(projectItem, setupFileUri)); + const dependencyFileUri = await projectItem.project.discoverDependencyFiles?.(); + if (dependencyFileUri) { + views.push(new ProjectDependencyFile(projectItem, dependencyFileUri)); } } diff --git a/src/features/views/treeViewItems.ts b/src/features/views/treeViewItems.ts index 9ec198fd4..6e0959d9d 100644 --- a/src/features/views/treeViewItems.ts +++ b/src/features/views/treeViewItems.ts @@ -291,7 +291,7 @@ export class PackageRootInfoTreeItem implements EnvTreeItem { export enum ProjectTreeItemKind { project = 'project', - setupFile = 'project-setup-file', + dependencyFile = 'project-dependency-file', environment = 'project-environment', none = 'project-no-environment', environmentInfo = 'environment-info', @@ -326,8 +326,8 @@ export class ProjectItem implements ProjectTreeItem { } } -export class ProjectSetupFile implements ProjectTreeItem { - public readonly kind = ProjectTreeItemKind.setupFile; +export class ProjectDependencyFile implements ProjectTreeItem { + public readonly kind = ProjectTreeItemKind.dependencyFile; public readonly id: string; public readonly treeItem: TreeItem; @@ -335,12 +335,12 @@ export class ProjectSetupFile implements ProjectTreeItem { public readonly parent: ProjectItem, public readonly uri: Uri, ) { - this.id = `${parent.id}>>>setup-file`; + this.id = `${parent.id}>>>dependency-file`; const item = new TreeItem(uri, TreeItemCollapsibleState.None); - item.contextValue = 'project-setup-file'; + item.contextValue = 'project-dependency-file'; item.command = { command: 'vscode.open', - title: l10n.t('Open Setup File'), + title: l10n.t('Open Dependency File'), arguments: [uri], }; this.treeItem = item; diff --git a/src/internal.api.ts b/src/internal.api.ts index d0bcc26a6..e86e892e6 100644 --- a/src/internal.api.ts +++ b/src/internal.api.ts @@ -555,7 +555,12 @@ export class PythonPackageImpl implements Package { } export class PythonProjectsImpl implements PythonProject { - private static readonly setupFileNames = ['pyproject.toml', 'setup.py', 'requirements.txt'] as const; + private static readonly dependencyFileNames = [ + 'requirements.txt', + 'pyproject.toml', + 'requirements.in', + 'environment.yml', + ] as const; name: string; uri: Uri; @@ -576,10 +581,10 @@ export class PythonProjectsImpl implements PythonProject { } /** - * Finds the preferred setup file at the project root. - * @returns The setup file URI, or `undefined` when no supported setup file exists. + * Finds the preferred dependency file at the project root. + * @returns The dependency file URI, or `undefined` when no supported dependency file exists. */ - async discoverProjectSetupFile(): Promise { + async discoverDependencyFiles(): Promise { let projectType: FileType; try { projectType = (await stat(this.uri)).type; @@ -587,17 +592,17 @@ export class PythonProjectsImpl implements PythonProject { return undefined; } - // A project URI may point directly to a setup file instead of its parent directory. + // A project URI may point directly to a dependency file instead of its parent directory. if (projectType !== FileType.Directory) { const fileName = path.posix.basename(this.uri.path); return projectType === FileType.File && - PythonProjectsImpl.setupFileNames.some((candidate) => candidate === fileName) + PythonProjectsImpl.dependencyFileNames.some((candidate) => candidate === fileName) ? this.uri : undefined; } - // Search directory candidates in setup-file priority order. - for (const fileName of PythonProjectsImpl.setupFileNames) { + // Search directory candidates in dependency-file priority order. + for (const fileName of PythonProjectsImpl.dependencyFileNames) { const candidate = this.uri.with({ path: path.posix.join(this.uri.path, fileName) }); try { const candidateType = (await stat(candidate)).type; @@ -605,7 +610,7 @@ export class PythonProjectsImpl implements PythonProject { return candidate; } } catch { - // Try the next supported setup file. + // Try the next supported dependency file. } } diff --git a/src/test/features/projectSetupFile.unit.test.ts b/src/test/features/projectDependencyFiles.unit.test.ts similarity index 56% rename from src/test/features/projectSetupFile.unit.test.ts rename to src/test/features/projectDependencyFiles.unit.test.ts index ded8ab6f6..d64f5f352 100644 --- a/src/test/features/projectSetupFile.unit.test.ts +++ b/src/test/features/projectDependencyFiles.unit.test.ts @@ -8,35 +8,35 @@ function fileStat(type: FileType): FileStat { return { type, ctime: 0, mtime: 0, size: 0 }; } -suite('Project setup file discovery', () => { +suite('Project dependency file discovery', () => { teardown(() => { sinon.restore(); }); - test('prefers pyproject.toml and preserves the project URI scheme', async () => { + test('prefers requirements.txt and preserves the project URI scheme', async () => { const projectUri = Uri.parse('vscode-remote://ssh-remote+host/workspace/project'); const project = new PythonProjectsImpl('project', projectUri); const statStub = sinon.stub(workspaceFs, 'stat').callsFake((uri) => { if (uri.toString() === projectUri.toString()) { return Promise.resolve(fileStat(FileType.Directory)); } - if (uri.path.endsWith('/pyproject.toml')) { + if (uri.path.endsWith('/requirements.txt')) { return Promise.resolve(fileStat(FileType.File)); } return Promise.reject(new Error('File not found')); }); - const result = await project.discoverProjectSetupFile(); + const result = await project.discoverDependencyFiles(); assert.strictEqual(result?.scheme, projectUri.scheme); assert.strictEqual(result?.authority, projectUri.authority); - assert.strictEqual(result?.path, '/workspace/project/pyproject.toml'); + assert.strictEqual(result?.path, '/workspace/project/requirements.txt'); assert.strictEqual(statStub.callCount, 2); }); - test('falls back to setup.py and requirements.txt', async () => { + test('falls back through generated dependency file names', async () => { const projectUri = Uri.file('/workspace/project'); - const availableFileNames = new Set(['setup.py']); + const availableFileNames = new Set(['pyproject.toml']); sinon.stub(workspaceFs, 'stat').callsFake((uri) => { if (uri.toString() === projectUri.toString()) { return Promise.resolve(fileStat(FileType.Directory)); @@ -47,18 +47,24 @@ suite('Project setup file discovery', () => { : Promise.reject(new Error('File not found')); }); - const setupProject = new PythonProjectsImpl('project', projectUri); - const setupFileUri = await setupProject.discoverProjectSetupFile(); - assert.strictEqual(setupFileUri?.path.endsWith('/setup.py'), true); + const pyproject = new PythonProjectsImpl('project', projectUri); + const pyprojectUri = await pyproject.discoverDependencyFiles(); + assert.strictEqual(pyprojectUri?.path.endsWith('/pyproject.toml'), true); availableFileNames.clear(); - availableFileNames.add('requirements.txt'); - const requirementsProject = new PythonProjectsImpl('project', projectUri); - const requirementsFileUri = await requirementsProject.discoverProjectSetupFile(); - assert.strictEqual(requirementsFileUri?.path.endsWith('/requirements.txt'), true); + availableFileNames.add('requirements.in'); + const requirements = new PythonProjectsImpl('project', projectUri); + const requirementsUri = await requirements.discoverDependencyFiles(); + assert.strictEqual(requirementsUri?.path.endsWith('/requirements.in'), true); + + availableFileNames.clear(); + availableFileNames.add('environment.yml'); + const environment = new PythonProjectsImpl('project', projectUri); + const environmentUri = await environment.discoverDependencyFiles(); + assert.strictEqual(environmentUri?.path.endsWith('/environment.yml'), true); }); - test('returns undefined when no setup file exists', async () => { + test('returns undefined when no dependency file exists', async () => { const projectUri = Uri.file('/workspace/project'); const project = new PythonProjectsImpl('project', projectUri); sinon.stub(workspaceFs, 'stat').callsFake((uri) => { @@ -67,22 +73,22 @@ suite('Project setup file discovery', () => { : Promise.reject(new Error('File not found')); }); - assert.strictEqual(await project.discoverProjectSetupFile(), undefined); + assert.strictEqual(await project.discoverDependencyFiles(), undefined); }); - test('does not append setup paths to a standalone Python file', async () => { + test('does not append dependency paths to a standalone Python file', async () => { const scriptUri = Uri.file('/workspace/script.py'); const project = new PythonProjectsImpl('script.py', scriptUri); sinon.stub(workspaceFs, 'stat').resolves(fileStat(FileType.File)); - assert.strictEqual(await project.discoverProjectSetupFile(), undefined); + assert.strictEqual(await project.discoverDependencyFiles(), undefined); }); - test('accepts a recognized setup file as the project URI', async () => { - const setupFileUri = Uri.file('/workspace/pyproject.toml'); - const project = new PythonProjectsImpl('pyproject.toml', setupFileUri); + test('accepts a recognized dependency file as the project URI', async () => { + const dependencyFileUri = Uri.file('/workspace/pyproject.toml'); + const project = new PythonProjectsImpl('pyproject.toml', dependencyFileUri); sinon.stub(workspaceFs, 'stat').resolves(fileStat(FileType.File)); - assert.strictEqual(await project.discoverProjectSetupFile(), setupFileUri); + assert.strictEqual(await project.discoverDependencyFiles(), dependencyFileUri); }); -}); +}); \ No newline at end of file diff --git a/src/test/features/views/treeViewItems.unit.test.ts b/src/test/features/views/treeViewItems.unit.test.ts index 44fc000b7..de42d7b7b 100644 --- a/src/test/features/views/treeViewItems.unit.test.ts +++ b/src/test/features/views/treeViewItems.unit.test.ts @@ -7,10 +7,10 @@ import { getEnvironmentParentDirName, NoPythonEnvTreeItem, PackageTreeItem, + ProjectDependencyFile, ProjectEnvironment, ProjectItem, ProjectPackage, - ProjectSetupFile, PythonEnvTreeItem, PythonGroupEnvTreeItem, } from '../../../features/views/treeViewItems'; @@ -81,17 +81,17 @@ function createMockManager( } suite('Test TreeView Items', () => { - suite('ProjectSetupFile', () => { - test('opens the setup file', () => { + suite('ProjectDependencyFile', () => { + test('opens the dependency file', () => { const parent = new ProjectItem({ name: 'project', uri: Uri.file('.') }); - const setupFileUri = Uri.file('pyproject.toml'); + const dependencyFileUri = Uri.file('pyproject.toml'); - const item = new ProjectSetupFile(parent, setupFileUri); + const item = new ProjectDependencyFile(parent, dependencyFileUri); assert.strictEqual(item.parent, parent); - assert.strictEqual(item.treeItem.resourceUri, setupFileUri); + assert.strictEqual(item.treeItem.resourceUri, dependencyFileUri); assert.strictEqual(item.treeItem.command?.command, 'vscode.open'); - assert.deepStrictEqual(item.treeItem.command?.arguments, [setupFileUri]); + assert.deepStrictEqual(item.treeItem.command?.arguments, [dependencyFileUri]); }); }); From 6307ef1bab022ffab94f2ca6f6c313f45d301717 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 16 Sep 2026 22:48:18 +0000 Subject: [PATCH 5/5] Resolve remaining merge conflict markers from main merge Co-authored-by: edvilme <5952839+edvilme@users.noreply.github.com> --- src/features/creators/autoFindProjects.ts | 10 +- src/features/projectManager.ts | 44 ++++ src/managers/common/registeredManagers.ts | 188 ------------------ .../projectDependencyFiles.unit.test.ts | 2 +- 4 files changed, 46 insertions(+), 198 deletions(-) diff --git a/src/features/creators/autoFindProjects.ts b/src/features/creators/autoFindProjects.ts index c4cc579b9..a9c5b7829 100644 --- a/src/features/creators/autoFindProjects.ts +++ b/src/features/creators/autoFindProjects.ts @@ -6,15 +6,7 @@ import { traceInfo } from '../../common/logging'; import { normalizePath } from '../../common/utils/pathUtils'; import { showErrorMessage, showQuickPickWithButtons, showWarningMessage } from '../../common/window.apis'; import { findFiles } from '../../common/workspace.apis'; -<<<<<<< HEAD -import { PythonProjectManager, PythonProjectsImpl } from '../../internal.api'; -======= -import { - PythonProjectManager, - PythonProjectsImpl, -} from '../projectManager'; -import { normalizePath } from '../../common/utils/pathUtils'; ->>>>>>> origin/main +import { PythonProjectManager, PythonProjectsImpl } from '../projectManager'; function getUniqueUri(uris: Uri[]): { label: string; diff --git a/src/features/projectManager.ts b/src/features/projectManager.ts index f5187ef9e..49a0538ed 100644 --- a/src/features/projectManager.ts +++ b/src/features/projectManager.ts @@ -51,6 +51,13 @@ export interface InlineScriptProjectRegistrationMarker { } export class PythonProjectsImpl implements PythonProject { + private static readonly dependencyFileNames = [ + 'requirements.txt', + 'pyproject.toml', + 'requirements.in', + 'environment.yml', + ] as const; + readonly name: string; readonly uri: Uri; readonly description?: string; @@ -68,6 +75,43 @@ export class PythonProjectsImpl implements PythonProject { this.tooltip = options?.tooltip ?? uri.fsPath; this.iconPath = options?.iconPath; } + + /** + * Finds the preferred dependency file at the project root. + * @returns The dependency file URI, or `undefined` when no supported dependency file exists. + */ + async discoverDependencyFiles(): Promise { + let projectType: FileType; + try { + projectType = (await stat(this.uri)).type; + } catch { + return undefined; + } + + // A project URI may point directly to a dependency file instead of its parent directory. + if (projectType !== FileType.Directory) { + const fileName = path.posix.basename(this.uri.path); + return projectType === FileType.File && + PythonProjectsImpl.dependencyFileNames.some((candidate) => candidate === fileName) + ? this.uri + : undefined; + } + + // Search directory candidates in dependency-file priority order. + for (const fileName of PythonProjectsImpl.dependencyFileNames) { + const candidate = this.uri.with({ path: path.posix.join(this.uri.path, fileName) }); + try { + const candidateType = (await stat(candidate)).type; + if (candidateType === FileType.File) { + return candidate; + } + } catch { + // Try the next supported dependency file. + } + } + + return undefined; + } } type ProjectArray = PythonProject[]; diff --git a/src/managers/common/registeredManagers.ts b/src/managers/common/registeredManagers.ts index 36466e98a..5daaff64d 100644 --- a/src/managers/common/registeredManagers.ts +++ b/src/managers/common/registeredManagers.ts @@ -2,20 +2,6 @@ // Licensed under the MIT License. import type { Pep440Version } from '@renovatebot/pep440'; -<<<<<<< HEAD:src/internal.api.ts -import * as path from 'path'; -import { - CancellationError, - Disposable, - Event, - FileType, - LogOutputChannel, - MarkdownString, - RelativePattern, - Uri, -} from 'vscode'; -import { -======= import { CancellationError, Disposable, LogOutputChannel, MarkdownString, RelativePattern } from 'vscode'; import { PackageVersionLookupNotSupportedError } from '../../publicErrors'; import { ISSUES_URL } from '../../common/constants'; @@ -26,7 +12,6 @@ import { EventNames } from '../../common/telemetry/constants'; import { classifyError, isTimeoutErrorType } from '../../common/telemetry/errorClassifier'; import { sendTelemetryEvent } from '../../common/telemetry/sender'; import type { ->>>>>>> origin/main:src/managers/common/registeredManagers.ts CreateEnvironmentOptions, CreateEnvironmentScope, DidChangeEnvironmentEventArgs, @@ -47,19 +32,7 @@ import type { RemoveEnvironmentOptions, ResolveEnvironmentContext, SetEnvironmentScope, -<<<<<<< HEAD:src/internal.api.ts -} from './api'; -import { ISSUES_URL } from './common/constants'; -import { CreateEnvironmentNotSupported, RemoveEnvironmentNotSupported } from './common/errors/NotSupportedError'; -import { traceWarn } from './common/logging'; -import { StopWatch } from './common/stopWatch'; -import { EventNames } from './common/telemetry/constants'; -import { classifyError, isTimeoutErrorType } from './common/telemetry/errorClassifier'; -import { sendTelemetryEvent } from './common/telemetry/sender'; -import { stat } from './common/workspace.fs.apis'; -======= } from '../../types'; ->>>>>>> origin/main:src/managers/common/registeredManagers.ts /* * Runtime wrappers around registered {@link EnvironmentManager} and {@link PackageManager} @@ -367,164 +340,3 @@ export class InternalPackageManager implements PackageManager { : `${packageName}==${version}`; } } -<<<<<<< HEAD:src/internal.api.ts - -export interface PythonProjectManager extends Disposable { - initialize(): void; - create( - name: string, - uri: Uri, - options?: { description?: string; tooltip?: string | MarkdownString; iconPath?: IconPath }, - ): PythonProject; - add(pyWorkspace: PythonProject | PythonProject[], options?: { persistSettings?: boolean }): Promise; - remove(pyWorkspace: PythonProject | PythonProject[]): void; - getProjects(uris?: Uri[]): ReadonlyArray; - get(uri: Uri): PythonProject | undefined; - onDidChangeProjects: Event; -} - -export type InlineScriptProjectRegistrationKind = 'created' | 'adopted'; - -export interface InlineScriptProjectRegistrationMarker { - readonly kind: InlineScriptProjectRegistrationKind; -} - -export interface PythonProjectSettings { - path: string; - envManager: string; - packageManager: string; - workspace?: string; - _inlineScriptRegistration?: InlineScriptProjectRegistrationMarker; -} - -export class PythonEnvironmentImpl implements PythonEnvironment { - public readonly name: string; - public readonly displayName: string; - public readonly shortDisplayName?: string; - public readonly displayPath: string; - public readonly version: string; - public readonly environmentPath: Uri; - public readonly description?: string; - public readonly tooltip?: string | MarkdownString; - public readonly iconPath?: IconPath; - public readonly execInfo: PythonEnvironmentExecutionInfo; - public readonly sysPrefix: string; - public readonly group?: string | EnvironmentGroupInfo; - public readonly error?: string; - - constructor( - public readonly envId: PythonEnvironmentId, - info: PythonEnvironmentInfo, - ) { - this.name = info.name; - this.displayName = info.displayName ?? this.name; - this.shortDisplayName = info.shortDisplayName; - this.displayPath = info.displayPath; - this.version = info.version; - this.environmentPath = info.environmentPath; - this.description = info.description; - this.tooltip = info.tooltip; - this.iconPath = info.iconPath; - this.execInfo = info.execInfo; - this.sysPrefix = info.sysPrefix; - this.group = info.group; - this.error = info.error; - } -} - -export class PythonPackageImpl implements Package { - public readonly name: string; - public readonly displayName: string; - public readonly version?: string; - public readonly description?: string; - public readonly tooltip?: string | MarkdownString; - public readonly iconPath?: IconPath; - public readonly uris?: readonly Uri[]; - - public readonly isTransitive?: boolean; - - constructor( - public readonly pkgId: PackageId, - info: PackageInfo, - ) { - this.name = info.name; - this.displayName = info.displayName ?? this.name; - this.version = info.version; - this.description = info.description; - this.tooltip = info.tooltip; - this.iconPath = info.iconPath; - this.uris = info.uris; - this.isTransitive = info.isTransitive; - } -} - -export class PythonProjectsImpl implements PythonProject { - private static readonly dependencyFileNames = [ - 'requirements.txt', - 'pyproject.toml', - 'requirements.in', - 'environment.yml', - ] as const; - - name: string; - uri: Uri; - description?: string; - tooltip?: string | MarkdownString; - iconPath?: IconPath; - - constructor( - name: string, - uri: Uri, - options?: { description?: string; tooltip?: string | MarkdownString; iconPath?: IconPath }, - ) { - this.name = name; - this.uri = uri; - this.description = options?.description ?? uri.fsPath; - this.tooltip = options?.tooltip ?? uri.fsPath; - this.iconPath = options?.iconPath; - } - - /** - * Finds the preferred dependency file at the project root. - * @returns The dependency file URI, or `undefined` when no supported dependency file exists. - */ - async discoverDependencyFiles(): Promise { - let projectType: FileType; - try { - projectType = (await stat(this.uri)).type; - } catch { - return undefined; - } - - // A project URI may point directly to a dependency file instead of its parent directory. - if (projectType !== FileType.Directory) { - const fileName = path.posix.basename(this.uri.path); - return projectType === FileType.File && - PythonProjectsImpl.dependencyFileNames.some((candidate) => candidate === fileName) - ? this.uri - : undefined; - } - - // Search directory candidates in dependency-file priority order. - for (const fileName of PythonProjectsImpl.dependencyFileNames) { - const candidate = this.uri.with({ path: path.posix.join(this.uri.path, fileName) }); - try { - const candidateType = (await stat(candidate)).type; - if (candidateType === FileType.File) { - return candidate; - } - } catch { - // Try the next supported dependency file. - } - } - - return undefined; - } -} - -export interface ProjectCreators extends Disposable { - registerPythonProjectCreator(creator: PythonProjectCreator): Disposable; - getProjectCreators(): PythonProjectCreator[]; -} -======= ->>>>>>> origin/main:src/managers/common/registeredManagers.ts diff --git a/src/test/features/projectDependencyFiles.unit.test.ts b/src/test/features/projectDependencyFiles.unit.test.ts index d64f5f352..efc354618 100644 --- a/src/test/features/projectDependencyFiles.unit.test.ts +++ b/src/test/features/projectDependencyFiles.unit.test.ts @@ -2,7 +2,7 @@ import assert from 'assert'; import * as sinon from 'sinon'; import { FileStat, FileType, Uri } from 'vscode'; import * as workspaceFs from '../../common/workspace.fs.apis'; -import { PythonProjectsImpl } from '../../internal.api'; +import { PythonProjectsImpl } from '../../features/projectManager'; function fileStat(type: FileType): FileStat { return { type, ctime: 0, mtime: 0, size: 0 };