@@ -495,6 +495,14 @@ def _check_type(d, field_name, cls):
495495 "{} field should be {}, not {}" .format (field_name , cls , type (d [field_name ]))
496496 )
497497
498+ def _check_types (d , field_name , cls_list ) -> None :
499+ if not isinstance (d [field_name ], cls_list ):
500+ raise ConfigError (
501+ "{} field should be {}, not {}" .format (
502+ field_name , ' or ' .join (map (str , cls_list )), type (d [field_name ])
503+ )
504+ )
505+
498506def _check_list_of_str (d , field_name ):
499507 if not isinstance (d [field_name ], list ) or not all (
500508 isinstance (e , str ) for e in d [field_name ]
@@ -577,30 +585,38 @@ def read_pep621_metadata(proj, path) -> LoadedConfig:
577585
578586 license_files = set ()
579587 if 'license' in proj :
580- _check_type (proj , 'license' , dict )
581- license_tbl = proj ['license' ]
582- unrec_keys = set (license_tbl .keys ()) - {'text' , 'file' }
583- if unrec_keys :
584- raise ConfigError (
585- "Unrecognised keys in [project.license]: {}" .format (unrec_keys )
586- )
588+ _check_types (proj , 'license' , (str , dict ))
589+ if isinstance (proj ['license' ], str ):
590+ md_dict ['license_expression' ] = normalize_license_expr (proj ['license' ])
591+ else :
592+ license_tbl = proj ['license' ]
593+ unrec_keys = set (license_tbl .keys ()) - {'text' , 'file' }
594+ if unrec_keys :
595+ raise ConfigError (
596+ "Unrecognised keys in [project.license]: {}" .format (unrec_keys )
597+ )
587598
588- # TODO: Do something with license info.
589- # The 'License' field in packaging metadata is a brief description of
590- # a license, not the full text or a file path. PEP 639 will improve on
591- # how licenses are recorded.
592- if 'file' in license_tbl :
593- if 'text' in license_tbl :
599+ # The 'License' field in packaging metadata is a brief description of
600+ # a license, not the full text or a file path.
601+ if 'file' in license_tbl :
602+ if 'text' in license_tbl :
603+ raise ConfigError (
604+ "[project.license] should specify file or text, not both"
605+ )
606+ license_f = license_tbl ['file' ]
607+ if isabs_ish (license_f ):
608+ raise ConfigError (
609+ f"License file path ({ license_f } ) cannot be an absolute path"
610+ )
611+ if not (path .parent / license_f ).is_file ():
612+ raise ConfigError (f"License file { license_f } does not exist" )
613+ license_files .add (license_tbl ['file' ])
614+ elif 'text' in license_tbl :
615+ pass
616+ else :
594617 raise ConfigError (
595- "[project.license] should specify file or text, not both "
618+ "file or text field required in [project.license] table "
596619 )
597- license_files .add (license_tbl ['file' ])
598- elif 'text' in license_tbl :
599- pass
600- else :
601- raise ConfigError (
602- "file or text field required in [project.license] table"
603- )
604620
605621 if 'license-files' in proj :
606622 _check_type (proj , 'license-files' , list )
@@ -635,6 +651,16 @@ def read_pep621_metadata(proj, path) -> LoadedConfig:
635651
636652 if 'classifiers' in proj :
637653 _check_list_of_str (proj , 'classifiers' )
654+ classifiers = proj ['classifiers' ]
655+ license_expr = md_dict .get ('license_expression' , None )
656+ if license_expr :
657+ for cl in classifiers :
658+ if not cl .startswith ('License :: ' ):
659+ continue
660+ raise ConfigError (
661+ "License classifier are deprecated in favor of the license expression. "
662+ "Remove the '{}' classifier" .format (cl )
663+ )
638664 md_dict ['classifiers' ] = proj ['classifiers' ]
639665
640666 if 'urls' in proj :
@@ -788,3 +814,36 @@ def isabs_ish(path):
788814 absolute paths, we also want to reject these odd halfway paths.
789815 """
790816 return os .path .isabs (path ) or path .startswith (('/' , '\\ ' ))
817+
818+
819+ def normalize_license_expr (s : str ):
820+ """Validate & normalise an SPDX license expression
821+
822+ For now this only handles simple expressions (referring to 1 license)
823+ """
824+ from ._spdx_data import licenses
825+ ls = s .lower ()
826+ if ls .startswith ('licenseref-' ):
827+ ref = s .partition ('-' )[2 ]
828+ if re .match (r'([a-zA-Z0-9\-.])+$' , ref ):
829+ # Normalise case of LicenseRef, leave the rest alone
830+ return "LicenseRef-" + ref
831+ raise ConfigError (
832+ "LicenseRef- license expression can only contain ASCII letters "
833+ "& digits, - and ."
834+ )
835+
836+ or_later = s .endswith ('+' )
837+ if or_later :
838+ ls = ls [:- 1 ]
839+
840+ try :
841+ info = licenses [ls ]
842+ except KeyError :
843+ if os .environ .get ('FLIT_ALLOW_INVALID' ):
844+ log .warning ("Invalid license ID {!r} allowed by FLIT_ALLOW_INVALID"
845+ .format (s ))
846+ return s
847+ raise ConfigError (f"{ s !r} is not a recognised SPDX license ID" )
848+
849+ return info ['id' ] + ('+' if or_later else '' )
0 commit comments