@@ -176,13 +176,6 @@ suite('pipUtils - validatePyproject', () => {
176176 verifyValidationError ( toml , 'Invalid package name "mypackage_" in pyproject.toml.' ) ;
177177 } ) ;
178178
179- test ( 'should reject empty package name' , ( ) => {
180- const toml : tomljs . JsonMap = {
181- project : { name : '' } as tomljs . JsonMap ,
182- } ;
183- verifyValidationError ( toml , 'Invalid package name "" in pyproject.toml.' ) ;
184- } ) ;
185-
186179 test ( 'should reject package name with special characters' , ( ) => {
187180 const toml : tomljs . JsonMap = {
188181 project : { name : 'my@package' } as tomljs . JsonMap ,
@@ -255,4 +248,226 @@ suite('pipUtils - validatePyproject', () => {
255248 verifyValidationError ( toml , undefined ) ;
256249 } ) ;
257250 } ) ;
251+
252+ suite ( 'validatePyprojectToml - Version Validation (PEP 440)' , ( ) => {
253+ interface VersionTestCase {
254+ version : string ;
255+ expectedError : string | undefined ;
256+ description : string ;
257+ }
258+
259+ function createVersionToml ( version : string ) : tomljs . JsonMap {
260+ return {
261+ project : { name : 'test' , version } as tomljs . JsonMap ,
262+ } ;
263+ }
264+
265+ const versionTestCases : VersionTestCase [ ] = [
266+ // Basic release versions
267+ { version : '1.0' , expectedError : undefined , description : 'simple version 1.0' } ,
268+ { version : '1.0.0' , expectedError : undefined , description : 'version with three parts 1.0.0' } ,
269+ { version : '1.2.3.4.5' , expectedError : undefined , description : 'version with many parts 1.2.3.4.5' } ,
270+ { version : '1.0.01' , expectedError : undefined , description : 'version with leading zeros 1.0.01' } ,
271+ { version : '0' , expectedError : undefined , description : 'single digit version 0' } ,
272+ { version : '2024.1.15' , expectedError : undefined , description : 'large version numbers 2024.1.15' } ,
273+
274+ // Epoch versions
275+ { version : '1!1.0' , expectedError : undefined , description : 'epoch version 1!1.0' } ,
276+ { version : '2!0.0.1' , expectedError : undefined , description : 'epoch version 2!0.0.1' } ,
277+ { version : '100!1.0.0' , expectedError : undefined , description : 'large epoch 100!1.0.0' } ,
278+
279+ // Pre-release versions - Alpha
280+ { version : '1.0a1' , expectedError : undefined , description : 'alpha version 1.0a1' } ,
281+ { version : '1.0.a1' , expectedError : undefined , description : 'alpha with dot separator 1.0.a1' } ,
282+ { version : '1.0-a1' , expectedError : undefined , description : 'alpha with hyphen separator 1.0-a1' } ,
283+ { version : '1.0_a1' , expectedError : undefined , description : 'alpha with underscore 1.0_a1' } ,
284+ { version : '1.0a' , expectedError : undefined , description : 'alpha without number 1.0a' } ,
285+ { version : '1.0alpha1' , expectedError : undefined , description : 'long form alpha 1.0alpha1' } ,
286+ { version : '1.0.alpha.1' , expectedError : undefined , description : 'alpha with separators 1.0.alpha.1' } ,
287+ { version : '1.0a999' , expectedError : undefined , description : 'alpha with large number 1.0a999' } ,
288+
289+ // Pre-release versions - Beta
290+ { version : '1.0b1' , expectedError : undefined , description : 'beta version 1.0b1' } ,
291+ { version : '1.0beta1' , expectedError : undefined , description : 'long form beta 1.0beta1' } ,
292+ { version : '1.0.beta.2' , expectedError : undefined , description : 'beta with separators 1.0.beta.2' } ,
293+ { version : '1.0b' , expectedError : undefined , description : 'beta without number 1.0b' } ,
294+
295+ // Pre-release versions - RC
296+ { version : '1.0rc1' , expectedError : undefined , description : 'rc version 1.0rc1' } ,
297+ { version : '1.0c1' , expectedError : undefined , description : 'c version 1.0c1' } ,
298+ { version : '1.0.rc.3' , expectedError : undefined , description : 'rc with separators 1.0.rc.3' } ,
299+ { version : '1.0rc' , expectedError : undefined , description : 'rc without number 1.0rc' } ,
300+
301+ // Pre-release versions - Other
302+ { version : '1.0preview1' , expectedError : undefined , description : 'preview version 1.0preview1' } ,
303+ { version : '1.0pre1' , expectedError : undefined , description : 'pre version 1.0pre1' } ,
304+ {
305+ version : '1.0-preview-2' ,
306+ expectedError : undefined ,
307+ description : 'preview with separators 1.0-preview-2' ,
308+ } ,
309+
310+ // Post-release versions
311+ { version : '1.0.post1' , expectedError : undefined , description : 'post version 1.0.post1' } ,
312+ { version : '1.0post1' , expectedError : undefined , description : 'post without dot 1.0post1' } ,
313+ { version : '1.0-post1' , expectedError : undefined , description : 'post with hyphen 1.0-post1' } ,
314+ { version : '1.0_post1' , expectedError : undefined , description : 'post with underscore 1.0_post1' } ,
315+ { version : '1.0.post' , expectedError : undefined , description : 'post without number 1.0.post' } ,
316+ { version : '1.0-1' , expectedError : undefined , description : 'implicit post version 1.0-1' } ,
317+ { version : '1.0-5' , expectedError : undefined , description : 'implicit post version 1.0-5' } ,
318+ { version : '1.0rev1' , expectedError : undefined , description : 'rev version 1.0rev1' } ,
319+ { version : '1.0r1' , expectedError : undefined , description : 'r version 1.0r1' } ,
320+ { version : '1.0.rev.2' , expectedError : undefined , description : 'rev with separators 1.0.rev.2' } ,
321+ { version : '1.0.post999' , expectedError : undefined , description : 'post with large number 1.0.post999' } ,
322+
323+ // Dev versions
324+ { version : '1.0.dev1' , expectedError : undefined , description : 'dev version 1.0.dev1' } ,
325+ { version : '1.0dev1' , expectedError : undefined , description : 'dev without dot 1.0dev1' } ,
326+ { version : '1.0-dev1' , expectedError : undefined , description : 'dev with hyphen 1.0-dev1' } ,
327+ { version : '1.0_dev1' , expectedError : undefined , description : 'dev with underscore 1.0_dev1' } ,
328+ { version : '1.0.dev' , expectedError : undefined , description : 'dev without number 1.0.dev' } ,
329+ { version : '1.0.dev999' , expectedError : undefined , description : 'dev with large number 1.0.dev999' } ,
330+
331+ // Local versions
332+ { version : '1.0+abc' , expectedError : undefined , description : 'local version 1.0+abc' } ,
333+ { version : '1.0+abc.def' , expectedError : undefined , description : 'local with dots 1.0+abc.def' } ,
334+ { version : '1.0+abc-def' , expectedError : undefined , description : 'local with hyphens 1.0+abc-def' } ,
335+ { version : '1.0+abc_def' , expectedError : undefined , description : 'local with underscores 1.0+abc_def' } ,
336+ { version : '1.0+abc.5' , expectedError : undefined , description : 'local with numbers 1.0+abc.5' } ,
337+ {
338+ version : '1.0+abc.def-ghi_jkl' ,
339+ expectedError : undefined ,
340+ description : 'local with mixed separators 1.0+abc.def-ghi_jkl' ,
341+ } ,
342+ { version : '1.0+001' , expectedError : undefined , description : 'numeric local version 1.0+001' } ,
343+ { version : '1.0+g1234567' , expectedError : undefined , description : 'git hash-like local 1.0+g1234567' } ,
344+
345+ // Combined versions
346+ { version : '1.0a1.post1' , expectedError : undefined , description : 'pre + post 1.0a1.post1' } ,
347+ {
348+ version : '1.0a1.post1.dev2' ,
349+ expectedError : undefined ,
350+ description : 'pre + post + dev 1.0a1.post1.dev2' ,
351+ } ,
352+ { version : '1.0.post1.dev2' , expectedError : undefined , description : 'post + dev 1.0.post1.dev2' } ,
353+ { version : '1.0a1.dev1' , expectedError : undefined , description : 'pre + dev 1.0a1.dev1' } ,
354+ { version : '1.0a1+local' , expectedError : undefined , description : 'pre + local 1.0a1+local' } ,
355+ { version : '1.0.post1+local' , expectedError : undefined , description : 'post + local 1.0.post1+local' } ,
356+ { version : '1.0.dev1+local' , expectedError : undefined , description : 'dev + local 1.0.dev1+local' } ,
357+ {
358+ version : '1!1.0a1.post1.dev2+abc' ,
359+ expectedError : undefined ,
360+ description : 'epoch + all components 1!1.0a1.post1.dev2+abc' ,
361+ } ,
362+ {
363+ version : '2!1.2.3rc4.post5.dev6+local.version' ,
364+ expectedError : undefined ,
365+ description : 'full complex version 2!1.2.3rc4.post5.dev6+local.version' ,
366+ } ,
367+ { version : '1.0rc1-1' , expectedError : undefined , description : 'rc + implicit post 1.0rc1-1' } ,
368+
369+ // Version with v prefix
370+ { version : 'v1.0' , expectedError : undefined , description : 'version with v prefix v1.0' } ,
371+ { version : 'v1.0.0' , expectedError : undefined , description : 'version with v prefix v1.0.0' } ,
372+ { version : 'v1.0a1' , expectedError : undefined , description : 'v prefix with pre-release v1.0a1' } ,
373+ { version : 'v1.0-1' , expectedError : undefined , description : 'v prefix with implicit post v1.0-1' } ,
374+ {
375+ version : 'v1!2.0rc1.post2.dev3+local' ,
376+ expectedError : undefined ,
377+ description : 'v prefix with all components v1!2.0rc1.post2.dev3+local' ,
378+ } ,
379+
380+ // Case insensitivity
381+ { version : '1.0A1' , expectedError : undefined , description : 'uppercase alpha 1.0A1' } ,
382+ { version : '1.0ALPHA1' , expectedError : undefined , description : 'uppercase ALPHA 1.0ALPHA1' } ,
383+ { version : '1.0Alpha1' , expectedError : undefined , description : 'mixed case Alpha 1.0Alpha1' } ,
384+ { version : '1.0POST1' , expectedError : undefined , description : 'uppercase POST 1.0POST1' } ,
385+ { version : '1.0DEV1' , expectedError : undefined , description : 'uppercase DEV 1.0DEV1' } ,
386+ { version : '1.0RC1' , expectedError : undefined , description : 'uppercase RC 1.0RC1' } ,
387+ { version : 'V1.0' , expectedError : undefined , description : 'uppercase V prefix V1.0' } ,
388+ {
389+ version : '1.0Alpha1.POST2.Dev3' ,
390+ expectedError : undefined ,
391+ description : 'mixed case components 1.0Alpha1.POST2.Dev3' ,
392+ } ,
393+
394+ // Invalid versions
395+ {
396+ version : '.1.0' ,
397+ expectedError : 'Invalid version ".1.0" in pyproject.toml.' ,
398+ description : 'starting with dot .1.0' ,
399+ } ,
400+ {
401+ version : '1.0.' ,
402+ expectedError : 'Invalid version "1.0." in pyproject.toml.' ,
403+ description : 'ending with dot 1.0.' ,
404+ } ,
405+ {
406+ version : 'abc' ,
407+ expectedError : 'Invalid version "abc" in pyproject.toml.' ,
408+ description : 'completely invalid abc' ,
409+ } ,
410+ { version : '' , expectedError : 'Version cannot be empty in pyproject.toml.' , description : 'empty version' } ,
411+ {
412+ version : '1..0' ,
413+ expectedError : 'Invalid version "1..0" in pyproject.toml.' ,
414+ description : 'double dots 1..0' ,
415+ } ,
416+ {
417+ version : '1.0 rc1' ,
418+ expectedError : 'Invalid version "1.0 rc1" in pyproject.toml.' ,
419+ description : 'spaces 1.0 rc1' ,
420+ } ,
421+ {
422+ version : '1.0gamma1' ,
423+ expectedError : 'Invalid version "1.0gamma1" in pyproject.toml.' ,
424+ description : 'invalid pre-release keyword 1.0gamma1' ,
425+ } ,
426+ {
427+ version : '1 1.0' ,
428+ expectedError : 'Invalid version "1 1.0" in pyproject.toml.' ,
429+ description : 'epoch without exclamation 1 1.0' ,
430+ } ,
431+ {
432+ version : '1.0local' ,
433+ expectedError : 'Invalid version "1.0local" in pyproject.toml.' ,
434+ description : 'local without plus 1.0local' ,
435+ } ,
436+ {
437+ version : '1.0+abc+def' ,
438+ expectedError : 'Invalid version "1.0+abc+def" in pyproject.toml.' ,
439+ description : 'multiple local markers 1.0+abc+def' ,
440+ } ,
441+ {
442+ version : '1!' ,
443+ expectedError : 'Invalid version "1!" in pyproject.toml.' ,
444+ description : 'only epoch 1!' ,
445+ } ,
446+ {
447+ version : '1.0--a1' ,
448+ expectedError : 'Invalid version "1.0--a1" in pyproject.toml.' ,
449+ description : 'invalid separator combinations 1.0--a1' ,
450+ } ,
451+ ] ;
452+
453+ versionTestCases . forEach ( ( { version, expectedError, description } ) => {
454+ test ( `should ${ expectedError ? 'reject' : 'accept' } ${ description } ` , ( ) => {
455+ const toml = createVersionToml ( version ) ;
456+ verifyValidationError ( toml , expectedError ) ;
457+ } ) ;
458+ } ) ;
459+
460+ // Edge cases
461+ test ( 'should accept when no version field exists' , ( ) => {
462+ const toml : tomljs . JsonMap = {
463+ project : { name : 'test' } as tomljs . JsonMap ,
464+ } ;
465+ verifyValidationError ( toml , undefined ) ;
466+ } ) ;
467+
468+ test ( 'should accept when no project section exists' , ( ) => {
469+ const toml : tomljs . JsonMap = { } ;
470+ verifyValidationError ( toml , undefined ) ;
471+ } ) ;
472+ } ) ;
258473} ) ;
0 commit comments