11import * as tomljs from '@iarna/toml' ;
22import * as fse from 'fs-extra' ;
33import * as path from 'path' ;
4- import { LogOutputChannel , ProgressLocation , QuickInputButtons , QuickPickItem , Uri } from 'vscode' ;
4+ import { l10n , LogOutputChannel , ProgressLocation , QuickInputButtons , QuickPickItem , Uri } from 'vscode' ;
55import { PackageManagementOptions , PythonEnvironment , PythonEnvironmentApi , PythonProject } from '../../api' ;
66import { EXTENSION_ROOT_DIR } from '../../common/constants' ;
77import { PackageManagement , Pickers , VenvManagerStrings } from '../../common/localize' ;
@@ -13,6 +13,88 @@ import { Installable } from '../common/types';
1313import { mergePackages } from '../common/utils' ;
1414import { refreshPipPackages } from './utils' ;
1515
16+ /**
17+ * Validates pyproject.toml fields according to PEP 508, PEP 440, PEP 621, PEP 517/518
18+ * Returns error message if invalid, undefined if valid
19+ */
20+ function validatePyprojectToml ( toml : tomljs . JsonMap , filePath : string ) : string | undefined {
21+ // 1. Validate package name (PEP 508)
22+ if ( toml . project && ( toml . project as tomljs . JsonMap ) . name ) {
23+ const name = ( toml . project as tomljs . JsonMap ) . name as string ;
24+ // PEP 508 regex: must start and end with a letter or digit, can contain -_.
25+ const nameRegex = / ^ ( [ a - z A - Z 0 - 9 ] | [ a - z A - Z 0 - 9 ] [ a - z A - Z 0 - 9 . _ - ] * [ a - z A - Z 0 - 9 ] ) $ / ;
26+ if ( ! nameRegex . test ( name ) ) {
27+ return l10n . t (
28+ 'Invalid package name "{0}" in {1}. Package names must start and end with a letter or digit and may only contain -, _, ., and alphanumeric characters. No spaces allowed. See PEP 508: https://peps.python.org/pep-0508/' ,
29+ name ,
30+ path . basename ( filePath ) ,
31+ ) ;
32+ }
33+ }
34+
35+ // 2. Validate version format (PEP 440)
36+ if ( toml . project && ( toml . project as tomljs . JsonMap ) . version ) {
37+ const version = ( toml . project as tomljs . JsonMap ) . version as string ;
38+ // PEP 440 simplified regex
39+ const versionRegex =
40+ / ^ ( [ 1 - 9 ] [ 0 - 9 ] * ! ) ? ( 0 | [ 1 - 9 ] [ 0 - 9 ] * ) ( \. ( 0 | [ 1 - 9 ] [ 0 - 9 ] * ) ) * ( ( a | b | r c ) ( 0 | [ 1 - 9 ] [ 0 - 9 ] * ) ) ? ( \. p o s t ( 0 | [ 1 - 9 ] [ 0 - 9 ] * ) ) ? ( \. d e v ( 0 | [ 1 - 9 ] [ 0 - 9 ] * ) ) ? $ / ;
41+ if ( ! versionRegex . test ( version ) ) {
42+ return l10n . t (
43+ 'Invalid version "{0}" in {1}. Versions must follow PEP 440 format (e.g., "1.0.0", "2.1a3"). See https://peps.python.org/pep-0440/' ,
44+ version ,
45+ path . basename ( filePath ) ,
46+ ) ;
47+ }
48+ }
49+
50+ // 3. Validate required fields (PEP 621)
51+ if ( toml . project ) {
52+ const project = toml . project as tomljs . JsonMap ;
53+ if ( ! project . name ) {
54+ return l10n . t (
55+ 'Missing required field "name" in [project] section of {0}. See PEP 621: https://peps.python.org/pep-0621/' ,
56+ path . basename ( filePath ) ,
57+ ) ;
58+ }
59+ }
60+
61+ // 4. Validate build system (PEP 517/518)
62+ if ( toml [ 'build-system' ] ) {
63+ const buildSystem = toml [ 'build-system' ] as tomljs . JsonMap ;
64+ if ( ! buildSystem . requires ) {
65+ return l10n . t (
66+ 'Missing required field "requires" in [build-system] section of {0}. See PEP 517: https://peps.python.org/pep-0517/' ,
67+ path . basename ( filePath ) ,
68+ ) ;
69+ }
70+ if ( ! buildSystem [ 'build-backend' ] ) {
71+ return l10n . t (
72+ 'Missing required field "build-backend" in [build-system] section of {0}. See PEP 518: https://peps.python.org/pep-0518/' ,
73+ path . basename ( filePath ) ,
74+ ) ;
75+ }
76+ }
77+
78+ // 5. Validate dependencies format (PEP 508)
79+ if ( toml . project && ( toml . project as tomljs . JsonMap ) . dependencies ) {
80+ const deps = ( toml . project as tomljs . JsonMap ) . dependencies as string [ ] ;
81+ if ( Array . isArray ( deps ) ) {
82+ for ( const dep of deps ) {
83+ // Basic check for common mistakes
84+ if ( dep . includes ( ' ' ) || / \s { 2 , } / . test ( dep ) ) {
85+ return l10n . t (
86+ 'Invalid dependency "{0}" in {1}. Contains extra whitespace. See PEP 508: https://peps.python.org/pep-0508/' ,
87+ dep ,
88+ path . basename ( filePath ) ,
89+ ) ;
90+ }
91+ }
92+ }
93+ }
94+
95+ return undefined ; // No errors
96+ }
97+
1698async function tomlParse ( fsPath : string , log ?: LogOutputChannel ) : Promise < tomljs . JsonMap > {
1799 try {
18100 const content = await fse . readFile ( fsPath , 'utf-8' ) ;
@@ -148,14 +230,37 @@ export interface PipPackages {
148230 uninstall : string [ ] ;
149231}
150232
233+ export interface ProjectInstallableResult {
234+ /**
235+ * List of installable packages from requirements.txt and pyproject.toml files
236+ */
237+ installables : Installable [ ] ;
238+
239+ /**
240+ * Validation error information if pyproject.toml validation failed
241+ */
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+ } ;
253+ }
254+
151255export async function getWorkspacePackagesToInstall (
152256 api : PythonEnvironmentApi ,
153257 options : PackageManagementOptions ,
154258 project ?: PythonProject [ ] ,
155259 environment ?: PythonEnvironment ,
156260 log ?: LogOutputChannel ,
157261) : Promise < PipPackages | undefined > {
158- const installable = ( await getProjectInstallable ( api , project ) ) ?? [ ] ;
262+ const result = await getProjectInstallable ( api , project ) ;
263+ const installable = result . installables ;
159264 let common = await getCommonPackages ( ) ;
160265 let installed : string [ ] | undefined ;
161266 if ( environment ) {
@@ -168,12 +273,14 @@ export async function getWorkspacePackagesToInstall(
168273export async function getProjectInstallable (
169274 api : PythonEnvironmentApi ,
170275 projects ?: PythonProject [ ] ,
171- ) : Promise < Installable [ ] > {
276+ ) : Promise < ProjectInstallableResult > {
172277 if ( ! projects ) {
173- return [ ] ;
278+ return { installables : [ ] } ;
174279 }
175280 const exclude = '**/{.venv*,.git,.nox,.tox,.conda,site-packages,__pypackages__}/**' ;
176281 const installable : Installable [ ] = [ ] ;
282+ let validationError : { message : string ; fileUri : Uri } | undefined ;
283+
177284 await withProgress (
178285 {
179286 location : ProgressLocation . Notification ,
@@ -204,6 +311,18 @@ export async function getProjectInstallable(
204311 filtered . map ( async ( uri ) => {
205312 if ( uri . fsPath . endsWith ( '.toml' ) ) {
206313 const toml = await tomlParse ( uri . fsPath ) ;
314+
315+ // Validate pyproject.toml and capture first error only
316+ if ( ! validationError ) {
317+ const error = validatePyprojectToml ( toml , uri . fsPath ) ;
318+ if ( error ) {
319+ validationError = {
320+ message : error ,
321+ fileUri : uri ,
322+ } ;
323+ }
324+ }
325+
207326 installable . push ( ...getTomlInstallable ( toml , uri ) ) ;
208327 } else {
209328 const name = path . basename ( uri . fsPath ) ;
@@ -219,7 +338,11 @@ export async function getProjectInstallable(
219338 ) ;
220339 } ,
221340 ) ;
222- return installable ;
341+
342+ return {
343+ installables : installable ,
344+ validationError,
345+ } ;
223346}
224347
225348export function isPipInstallCommand ( command : string ) : boolean {
0 commit comments