Skip to content
Merged
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
21 changes: 19 additions & 2 deletions src/pixi/envManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
import { EventEmitter, LogOutputChannel, MarkdownString, ProgressLocation, ThemeIcon, Uri, window } from 'vscode';

import { createDeferred, Deferred } from '../common/deferred';
import { traceVerbose } from '../common/logging';
import { traceInfo, traceVerbose } from '../common/logging';
import { resolvePixiProjectPaths } from '../common/searchPaths';
import { PIXI_MANAGER_ID } from '../common/utils';
import { PixiEnvironment } from './types';
Expand Down Expand Up @@ -122,12 +122,14 @@ export class PixiEnvManager implements EnvironmentManager {
}

async set(scope: SetEnvironmentScope, environment?: PythonEnvironment) {
traceVerbose(`Called set with scope: ${scope}, environment: ${JSON.stringify(environment)}`);
const environmentName = environment?.displayName ?? environment?.envId.id ?? 'none';
traceVerbose(`Setting ${scope === undefined ? 'global' : 'project'} Pixi environment to '${environmentName}'`);

if (scope === undefined) {
await setGlobalEnvId(environment?.envId.id);
this.triggerDidChangeEnvironment(undefined, this.globalEnv, environment);
this.globalEnv = environment;
traceInfo(`Activated global environment '${environmentName}'`);
return;
}

Expand All @@ -150,6 +152,10 @@ export class PixiEnvManager implements EnvironmentManager {

await setProjectEnvId(projectPath, environment?.envId.id);
this.triggerDidChangeEnvironment(project.uri, oldEnv, environment);
const projectName = project.name ?? projectPath;
traceInfo(
`${environment ? 'Activated' : 'Cleared'} environment '${environmentName}' for project '${projectName}'`,
);
}
}

Expand Down Expand Up @@ -187,6 +193,7 @@ export class PixiEnvManager implements EnvironmentManager {
}

private async refreshAll(): Promise<void> {
traceInfo('Discovering Pixi environments across registered projects and search paths');
await window.withProgress(
{
location: ProgressLocation.Window,
Expand All @@ -202,6 +209,7 @@ export class PixiEnvManager implements EnvironmentManager {

const searchPathRoots = await resolvePixiProjectPaths();
const projectPaths = new Set([...projectMap.keys(), ...searchPathRoots]);
traceVerbose(`Refreshing Pixi environments for ${projectPaths.size} project path(s)`);

const changes: DidChangeEnvironmentsEventArgs = [];

Expand All @@ -216,6 +224,13 @@ export class PixiEnvManager implements EnvironmentManager {
);

this._onDidChangeEnvironments.fire(changes);
const environmentCount = [...this.projectToEnvs.values()].reduce(
(count, envs) => count + envs.length,
0,
);
traceInfo(
`Discovered ${environmentCount} Pixi environment(s); ${changes.length} environment change(s)`,
);

const envLookup = this.buildEnvLookup();

Expand Down Expand Up @@ -254,8 +269,10 @@ export class PixiEnvManager implements EnvironmentManager {
}

const projectPath = project.uri.fsPath;
traceInfo(`Refreshing Pixi environments for project '${project.name ?? projectPath}'`);
const oldEnvs = this.projectToEnvs.get(projectPath) || [];
const newEnvs = await refreshPixi(projectPath);
traceInfo(`Found ${newEnvs.length} Pixi environment(s) for project '${project.name ?? projectPath}'`);

this.projectToEnvs.set(projectPath, newEnvs);
this._onDidChangeEnvironments.fire(this.diffEnvironments(oldEnvs, newEnvs));
Expand Down
9 changes: 7 additions & 2 deletions src/pixi/projectManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
window,
} from 'vscode';

import { traceInfo, traceVerbose } from '../common/logging';
import { traceError, traceInfo, traceVerbose } from '../common/logging';
import { PixiEnvironment } from './types';
import { listPixiPackages, pixiPkgsToPackages } from './utils';

Expand Down Expand Up @@ -90,8 +90,13 @@ export class PixiPackageManager implements PackageManager, Disposable {

environment.packages = after;
this.triggerOnDidChangePackages(environment, before, after);
traceInfo(
`Refreshed packages for '${environment.name}' in '${projectPath}': ${after.length} package(s)`,
);
} catch (error) {
traceInfo(`Failed to refresh packages for environment '${environment.name}': ${error}`);
traceError(
`Failed to refresh packages for environment '${environment.name}' in '${projectPath}': ${error}`,
);
}
},
);
Expand Down
15 changes: 10 additions & 5 deletions src/pixi/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import which from 'which';
import { createDeferred } from '../common/deferred';
import { quoteArgs, quoteStringIfNecessary } from '../common/execUtils';
import { findPythonExecutable } from '../common/findPython';
import { traceError, traceInfo, traceVerbose } from '../common/logging';
import { traceError, traceVerbose } from '../common/logging';
import { getWorkspacePersistentState } from '../common/persistentState';
import { PIXI_MANAGER_ID, untildify } from '../common/utils';
import { PixiEnvironment, PixiInfo, PixiPackage } from './types';
Expand Down Expand Up @@ -64,6 +64,8 @@ async function _runPixi(
): Promise<string> {
const deferred = createDeferred<string>();
args = quoteArgs(args);
const command = `${quoteStringIfNecessary(pixi)} ${args.join(' ')}`;
traceVerbose(`Running '${command}'${options?.cwd ? ` in '${options.cwd}'` : ''}`);
const proc = ch.spawn(quoteStringIfNecessary(pixi), args, { shell: true, ...options });

const cancelDisposable = token?.onCancellationRequested(() => {
Expand All @@ -81,7 +83,6 @@ async function _runPixi(
proc.stderr?.on('data', (data) => {
const d = data.toString('utf-8');
stderr += d;
traceError(d.trim());
});
proc.on('error', (err) => {
deferred.reject(err);
Expand All @@ -92,7 +93,9 @@ async function _runPixi(
proc.on('close', () => {
cancelDisposable?.dispose();
if (exitCode !== 0) {
deferred.reject(new Error(`Failed to run "pixi ${args.join(' ')}":\n ${stderr}`));
const message = `Pixi command '${command}' failed with exit code ${exitCode ?? 'unknown'}${stderr.trim() ? `:\n${stderr.trim()}` : ''}`;
traceError(message);
deferred.reject(new Error(message));
} else {
deferred.resolve(stdout);
}
Expand Down Expand Up @@ -138,7 +141,9 @@ export async function refreshPixi(projectPath: string): Promise<PixiEnvironment[
try {
pixiPackages = await listPixiPackages(pixiEnv.name, projectPath);
} catch (error) {
traceInfo(`Skipping Pixi environment '${pixiEnv.name}': ${error}`);
traceError(
`Could not list packages for Pixi environment '${projectName}:${pixiEnv.name}' in '${projectPath}': ${error}`,
);
return null;
}

Expand Down Expand Up @@ -190,7 +195,7 @@ export async function refreshPixi(projectPath: string): Promise<PixiEnvironment[

return results.filter((env): env is PixiEnvironment => env !== null);
} catch (error) {
traceInfo(`Failed to get pixi environments: ${error}`);
traceError(`Failed to discover Pixi environments in '${projectPath}': ${error}`);
return [];
}
}
Expand Down
22 changes: 19 additions & 3 deletions src/test/pixi.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ type SpawnResult = { stdout?: string; stderr?: string; exitCode?: number };

const calls: SpawnCall[] = [];
const infoMessages: string[] = [];
const errorMessages: string[] = [];
let configuredExecutable = 'pixi';
let workspaceFolders: { uri: { fsPath: string } }[] = [];
let respond: (call: SpawnCall) => SpawnResult = () => ({ exitCode: 0 });
Expand Down Expand Up @@ -99,13 +100,14 @@ loader._load = originalLoad;

registerLogger({
info: (message: string) => void infoMessages.push(message),
error: () => undefined,
error: (message: string) => void errorMessages.push(message),
debug: () => undefined,
} as unknown as Parameters<typeof registerLogger>[0]);

test('an unsupported Pixi environment does not hide valid Python environments', async () => {
calls.length = 0;
infoMessages.length = 0;
errorMessages.length = 0;
workspaceFolders = [{ uri: { fsPath: path.resolve('workspace') } }];
const projectPath = path.resolve('workspace', 'project');
respond = ({ args }) => {
Expand Down Expand Up @@ -136,12 +138,19 @@ test('an unsupported Pixi environment does not hide valid Python environments',
);
assert.equal(calls.length, 4);
assert.ok(calls.every((call) => call.options.cwd === projectPath));
assert.ok(infoMessages.some((message) => message.includes("Skipping Pixi environment 'foreign'")));
assert.ok(
errorMessages.some(
(message) =>
message.includes("Could not list packages for Pixi environment 'project:foreign'") &&
message.includes(projectPath),
),
);
});

test('package refresh uses the Pixi environment name and keeps existing packages on failure', async () => {
calls.length = 0;
infoMessages.length = 0;
errorMessages.length = 0;
const projectPath = path.resolve('workspace', 'project');
const prefix = path.join(projectPath, '.pixi', 'envs', 'default');
const packages = [{ name: 'python', version: '3.12.1' }];
Expand All @@ -164,7 +173,14 @@ test('package refresh uses the Pixi environment name and keeps existing packages
assert.equal(environment.packages, packages);
assert.equal(calls[0].args.at(-1), 'default');
assert.equal(calls[0].options.cwd, projectPath);
assert.ok(infoMessages.some((message) => message.includes('Failed to refresh packages')));
assert.ok(
errorMessages.some((message) => message.includes('Pixi command') && message.includes('Pixi list failed')),
);
assert.ok(
errorMessages.some(
(message) => message.includes('Failed to refresh packages') && message.includes(projectPath),
),
);

respond = () => ({ stdout: JSON.stringify([{ name: 'python', version: '3.13.0', is_explicit: true }]) });
await manager.refresh(environment);
Expand Down
Loading