Skip to content

Commit f1e4486

Browse files
committed
add shouldProceedAfterPyprojectValidation
1 parent 62bfe63 commit f1e4486

4 files changed

Lines changed: 102 additions & 20 deletions

File tree

src/common/localize.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,16 @@ export namespace Pickers {
6464
export const selectProject = l10n.t('Select a project, folder or script');
6565
export const selectProjects = l10n.t('Select one or more projects, folders or scripts');
6666
}
67+
68+
export namespace pyProject {
69+
export const validationError = l10n.t('Invalid pyproject.toml');
70+
export const validationErrorAction = l10n.t(
71+
'The pyproject.toml file has formatting errors. What would you like to do?',
72+
);
73+
export const openFile = l10n.t('Open pyproject.toml');
74+
export const continueAnyway = l10n.t('Continue Anyway');
75+
export const cancel = l10n.t('Cancel');
76+
}
6777
}
6878

6979
export namespace ProjectViews {

src/managers/builtin/pipUtils.ts

Lines changed: 72 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as tomljs from '@iarna/toml';
22
import * as fse from 'fs-extra';
33
import * as path from 'path';
4-
import { l10n, LogOutputChannel, ProgressLocation, QuickInputButtons, QuickPickItem, Uri } from 'vscode';
4+
import { l10n, LogOutputChannel, ProgressLocation, QuickInputButtons, QuickPickItem, Uri, window } from 'vscode';
55
import { PackageManagementOptions, PythonEnvironment, PythonEnvironmentApi, PythonProject } from '../../api';
66
import { EXTENSION_ROOT_DIR } from '../../common/constants';
77
import { PackageManagement, Pickers, VenvManagerStrings } from '../../common/localize';
@@ -160,11 +160,12 @@ async function getCommonPackages(): Promise<Installable[]> {
160160
}
161161

162162
async function selectWorkspaceOrCommon(
163-
installable: Installable[],
163+
installableResult: ProjectInstallableResult,
164164
common: Installable[],
165165
showSkipOption: boolean,
166166
installed: string[],
167167
): Promise<PipPackages | undefined> {
168+
const installable = installableResult.installables;
168169
if (installable.length === 0 && common.length === 0) {
169170
return undefined;
170171
}
@@ -206,7 +207,20 @@ async function selectWorkspaceOrCommon(
206207
if (selected && !Array.isArray(selected)) {
207208
try {
208209
if (selected.label === PackageManagement.workspaceDependencies) {
209-
return await selectFromInstallableToInstall(installable, undefined, { showBackButton });
210+
const selectedInstallables = await selectFromInstallableToInstall(installable, undefined, {
211+
showBackButton,
212+
});
213+
214+
const validationError = installableResult.validationError;
215+
const shouldProceed = await shouldProceedAfterPyprojectValidation(
216+
validationError,
217+
selectedInstallables?.install ?? [],
218+
);
219+
if (!shouldProceed) {
220+
return undefined;
221+
}
222+
223+
return selectedInstallables;
210224
} else if (selected.label === PackageManagement.searchCommonPackages) {
211225
return await selectFromCommonPackagesToInstall(common, installed, undefined, { showBackButton });
212226
} else if (selected.label === PackageManagement.skipPackageInstallation) {
@@ -218,7 +232,7 @@ async function selectWorkspaceOrCommon(
218232
// eslint-disable-next-line @typescript-eslint/no-explicit-any
219233
} catch (ex: any) {
220234
if (ex === QuickInputButtons.Back) {
221-
return selectWorkspaceOrCommon(installable, common, showSkipOption, installed);
235+
return selectWorkspaceOrCommon(installableResult, common, showSkipOption, installed);
222236
}
223237
}
224238
}
@@ -239,17 +253,19 @@ export interface ProjectInstallableResult {
239253
/**
240254
* Validation error information if pyproject.toml validation failed
241255
*/
242-
validationError?: {
243-
/**
244-
* Human-readable error message describing the validation issue
245-
*/
246-
message: string;
247-
248-
/**
249-
* URI to the pyproject.toml file that has the validation error
250-
*/
251-
fileUri: Uri;
252-
};
256+
validationError?: ValidationError;
257+
}
258+
259+
export interface ValidationError {
260+
/**
261+
* Human-readable error message describing the validation issue
262+
*/
263+
message: string;
264+
265+
/**
266+
* URI to the pyproject.toml file that has the validation error
267+
*/
268+
fileUri: Uri;
253269
}
254270

255271
export async function getWorkspacePackagesToInstall(
@@ -259,15 +275,14 @@ export async function getWorkspacePackagesToInstall(
259275
environment?: PythonEnvironment,
260276
log?: LogOutputChannel,
261277
): Promise<PipPackages | undefined> {
262-
const result = await getProjectInstallable(api, project);
263-
const installable = result.installables;
278+
const installableResult = await getProjectInstallable(api, project);
264279
let common = await getCommonPackages();
265280
let installed: string[] | undefined;
266281
if (environment) {
267282
installed = (await refreshPipPackages(environment, log, { showProgress: true }))?.map((pkg) => pkg.name);
268283
common = mergePackages(common, installed ?? []);
269284
}
270-
return selectWorkspaceOrCommon(installable, common, !!options.showSkipOption, installed ?? []);
285+
return selectWorkspaceOrCommon(installableResult, common, !!options.showSkipOption, installed ?? []);
271286
}
272287

273288
export async function getProjectInstallable(
@@ -345,6 +360,45 @@ export async function getProjectInstallable(
345360
};
346361
}
347362

363+
export async function shouldProceedAfterPyprojectValidation(
364+
validationError: ValidationError | undefined,
365+
install: string[],
366+
): Promise<boolean> {
367+
// 1. If no validation error or no installables selected, proceed
368+
if (!validationError || install.length === 0) {
369+
return true;
370+
}
371+
372+
const selectedTomlInstallables = install.some((arg, index, arr) => arg === '-e' && index + 1 < arr.length);
373+
if (!selectedTomlInstallables) {
374+
// 2. If no toml installables selected, proceed
375+
return true;
376+
}
377+
378+
// 3. Otherwise, show error message and ask user what to do
379+
const openButton = { title: Pickers.pyProject.openFile };
380+
const continueButton = { title: Pickers.pyProject.continueAnyway };
381+
const cancelButton = { title: Pickers.pyProject.cancel, isCloseAffordance: true };
382+
383+
const selection = await window.showErrorMessage(
384+
validationError.message,
385+
{ modal: true },
386+
openButton,
387+
continueButton,
388+
cancelButton,
389+
);
390+
391+
if (selection === continueButton) {
392+
return true;
393+
}
394+
395+
if (selection === openButton) {
396+
await window.showTextDocument(validationError.fileUri);
397+
}
398+
399+
return false;
400+
}
401+
348402
export function isPipInstallCommand(command: string): boolean {
349403
// Regex to match pip install commands, capturing variations like:
350404
// pip install package

src/managers/builtin/venvStepBasedFlow.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@ import { EventNames } from '../../common/telemetry/constants';
77
import { sendTelemetryEvent } from '../../common/telemetry/sender';
88
import { showInputBoxWithButtons, showQuickPickWithButtons } from '../../common/window.apis';
99
import { NativePythonFinder } from '../common/nativePythonFinder';
10-
import { getProjectInstallable, getWorkspacePackagesToInstall, PipPackages } from './pipUtils';
10+
import {
11+
getProjectInstallable,
12+
getWorkspacePackagesToInstall,
13+
PipPackages,
14+
shouldProceedAfterPyprojectValidation,
15+
} from './pipUtils';
1116
import { CreateEnvironmentResult, createWithProgress, ensureGlobalEnv } from './venvUtils';
1217

1318
/**
@@ -342,6 +347,13 @@ export async function createStepBasedVenvFlow(
342347
if (options.additionalPackages) {
343348
allPackages.push(...options.additionalPackages);
344349
}
350+
351+
const validationError = result.validationError;
352+
const shouldProceed = await shouldProceedAfterPyprojectValidation(validationError, allPackages);
353+
if (!shouldProceed) {
354+
return undefined;
355+
}
356+
345357
return await createWithProgress(nativeFinder, api, log, manager, state.basePython, venvRoot, quickEnvPath, {
346358
install: allPackages,
347359
uninstall: [],

src/managers/builtin/venvUtils.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
} from '../common/nativePythonFinder';
2727
import { getShellActivationCommands, shortVersion, sortEnvironments } from '../common/utils';
2828
import { runPython, runUV, shouldUseUv } from './helpers';
29-
import { getProjectInstallable, PipPackages } from './pipUtils';
29+
import { getProjectInstallable, PipPackages, shouldProceedAfterPyprojectValidation } from './pipUtils';
3030
import { resolveSystemPythonEnvironmentPath } from './utils';
3131
import { addUvEnvironment, removeUvEnvironment, UV_ENVS_KEY } from './uvEnvironments';
3232
import { createStepBasedVenvFlow } from './venvStepBasedFlow';
@@ -404,6 +404,12 @@ export async function quickCreateVenv(
404404
allPackages.push(...additionalPackages);
405405
}
406406

407+
const validationError = result.validationError;
408+
const shouldProceed = await shouldProceedAfterPyprojectValidation(validationError, allPackages);
409+
if (!shouldProceed) {
410+
return undefined;
411+
}
412+
407413
// Check if .venv already exists
408414
let venvPath = path.join(venvRoot.fsPath, '.venv');
409415
if (await fsapi.pathExists(venvPath)) {

0 commit comments

Comments
 (0)