Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions src/features/pythonApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
} from '../api';
import { traceError, traceInfo } from '../common/logging';
import { pickEnvironmentManager } from '../common/pickers/managers';
import { timeout } from '../common/utils/asyncUtils';
import { createDeferred } from '../common/utils/deferred';
import { checkUri } from '../common/utils/pathUtils';
import { handlePythonPath } from '../common/utils/pythonPath';
Expand All @@ -44,7 +45,6 @@ import {
PythonPackageImpl,
PythonProjectManager,
} from '../internal.api';
import { timeout } from '../common/utils/asyncUtils';
import { waitForAllEnvManagers, waitForEnvManager, waitForEnvManagerId } from './common/managerReady';
import { EnvVarManager } from './execution/envVariableManager';
import { runAsTask } from './execution/runAsTask';
Expand All @@ -58,12 +58,15 @@ import { TerminalManager } from './terminal/terminalManager';
const GET_ENVIRONMENT_TIMEOUT_MS = 1000;
const GET_ENVIRONMENT_TIMED_OUT = Symbol('getEnvironmentTimedOut');

class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
export class PythonEnvironmentApiImpl implements PythonEnvironmentApi {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we are exporting this?

private readonly _onDidChangeEnvironments = new EventEmitter<DidChangeEnvironmentsEventArgs>();
private readonly _onDidChangeEnvironment = new EventEmitter<DidChangeEnvironmentEventArgs>();
private readonly _onDidChangePythonProjects = new EventEmitter<DidChangePythonProjectsEventArgs>();
private readonly _onDidChangePackages = new EventEmitter<DidChangePackagesEventArgs>();
private readonly _onDidChangeEnvironmentVariables = new EventEmitter<DidChangeEnvironmentVariablesEventArgs>();
// Tracks the last-known project set so we can compute an added/removed delta
// whenever the underlying project manager reports a change (fix for #1599).
private previousProjects: readonly PythonProject[] = [];

constructor(
private readonly envManagers: EnvironmentManagers,
Expand All @@ -73,6 +76,8 @@ class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
private readonly envVarManager: EnvVarManager,
private readonly disposables: Disposable[] = [],
) {
this.previousProjects = this.projectManager.getProjects();

this.disposables.push(
this._onDidChangeEnvironment,
this._onDidChangeEnvironments,
Expand All @@ -87,6 +92,22 @@ class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
);
}),
this.envVarManager.onDidChangeEnvironmentVariables((e) => this._onDidChangeEnvironmentVariables.fire(e)),
this.projectManager.onDidChangeProjects(() => {
const current = this.projectManager.getProjects();
const added = current.filter(
(c) => !this.previousProjects.some((p) => p.uri.toString() === c.uri.toString()),
);
const removed = this.previousProjects.filter(
(p) => !current.some((c) => c.uri.toString() === p.uri.toString()),
);
this.previousProjects = current;
if (added.length > 0 || removed.length > 0) {
traceInfo(
`Python API: Projects changed. Added: ${added.length}, Removed: ${removed.length}`,
);
this._onDidChangePythonProjects.fire({ added, removed });
}
}),
);
}

Expand Down
54 changes: 54 additions & 0 deletions src/test/features/pythonApi.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as assert from 'assert';
import { EventEmitter, Uri } from 'vscode';
import { PythonProject } from '../../api';
import { PythonEnvironmentApiImpl } from '../../features/pythonApi';
import { PythonProjectManager } from '../../internal.api';

suite('PythonEnvironmentApiImpl - onDidChangePythonProjects', () => {
test('Fires event with correct added and removed projects', async () => {
// 1. Create a mock EventEmitter to simulate the internal project manager
const onDidChangeProjectsEmitter = new EventEmitter<void>();

// 2. Mock the PythonProjectManager
let currentProjects: PythonProject[] = [];
const mockProjectManager = {
getProjects: () => currentProjects,
onDidChangeProjects: onDidChangeProjectsEmitter.event,
// ... mock other required methods like add/remove if necessary
} as unknown as PythonProjectManager;

// 3. Mock the other required constructor arguments (envManagers, terminalManager, etc.)
const mockEnvManagers = { onDidChangeActiveEnvironment: new EventEmitter().event } as any;

Check failure on line 21 in src/test/features/pythonApi.unit.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
const mockProjectCreators = {} as any;

Check failure on line 22 in src/test/features/pythonApi.unit.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
const mockTerminalManager = {} as any;

Check failure on line 23 in src/test/features/pythonApi.unit.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
const mockEnvVarManager = { onDidChangeEnvironmentVariables: new EventEmitter().event } as any;

Check failure on line 24 in src/test/features/pythonApi.unit.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type

// 4. Initialize the API instance
const api = new PythonEnvironmentApiImpl(
mockEnvManagers,
mockProjectManager,
mockProjectCreators,
mockTerminalManager,
mockEnvVarManager
);

// 5. Listen to the public event we are testing
let firedEventPayload: any = null;

Check failure on line 36 in src/test/features/pythonApi.unit.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
api.onDidChangePythonProjects((e: any) => {

Check failure on line 37 in src/test/features/pythonApi.unit.test.ts

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
firedEventPayload = e;
});

// 6. Simulate adding a project
const newProject = { uri: Uri.file('/fake/path') } as PythonProject;
currentProjects = [newProject]; // Update the mock's state

// Fire the internal event
onDidChangeProjectsEmitter.fire();

// 7. Assert the public event fired with the correct delta
assert.ok(firedEventPayload, 'Event should have fired');
assert.strictEqual(firedEventPayload.added.length, 1, 'Should have 1 added project');
assert.strictEqual(firedEventPayload.added[0].uri.fsPath, newProject.uri.fsPath);
assert.strictEqual(firedEventPayload.removed.length, 0, 'Should have 0 removed projects');
});
});
Loading