@@ -60,6 +60,7 @@ class ConfigError(ValueError):
6060 'readme' ,
6161 'requires-python' ,
6262 'license' ,
63+ 'license-files' ,
6364 'authors' ,
6465 'maintainers' ,
6566 'keywords' ,
@@ -73,6 +74,9 @@ class ConfigError(ValueError):
7374 'dynamic' ,
7475}
7576
77+ default_license_files_globs = ['COPYING*' , 'LICEN[CS]E*' ]
78+ license_files_allowed_chars = re .compile (r'^[\w\-\.\/\*\?\[\]]+$' )
79+
7680
7781def read_flit_config (path ):
7882 """Read and check the `pyproject.toml` file with data about the package.
@@ -427,6 +431,15 @@ def _prep_metadata(md_sect, path):
427431 # For internal use, record the main requirements as a '.none' extra.
428432 res .reqs_by_extra ['.none' ] = reqs_noextra
429433
434+ if path :
435+ license_files = sorted (
436+ _license_files_from_globs (
437+ path .parent , default_license_files_globs , warn_no_files = False
438+ )
439+ )
440+ res .referenced_files .extend (license_files )
441+ md_dict ['license_files' ] = license_files
442+
430443 return res
431444
432445def _expand_requires_extra (re ):
@@ -439,6 +452,43 @@ def _expand_requires_extra(re):
439452 yield '{} ; extra == "{}"' .format (req , extra )
440453
441454
455+ def _license_files_from_globs (project_dir : Path , globs , warn_no_files = True ):
456+ license_files = set ()
457+ for pattern in globs :
458+ if isabs_ish (pattern ):
459+ raise ConfigError (
460+ "Invalid glob pattern for [project.license-files]: '{}'. "
461+ "Pattern must not start with '/'." .format (pattern )
462+ )
463+ if ".." in pattern :
464+ raise ConfigError (
465+ "Invalid glob pattern for [project.license-files]: '{}'. "
466+ "Pattern must not contain '..'" .format (pattern )
467+ )
468+ if license_files_allowed_chars .match (pattern ) is None :
469+ raise ConfigError (
470+ "Invalid glob pattern for [project.license-files]: '{}'. "
471+ "Pattern contains invalid characters. "
472+ "https://packaging.python.org/en/latest/specifications/pyproject-toml/#license-files"
473+ )
474+ try :
475+ files = [
476+ str (file .relative_to (project_dir )).replace (osp .sep , "/" )
477+ for file in project_dir .glob (pattern )
478+ if file .is_file ()
479+ ]
480+ except ValueError as ex :
481+ raise ConfigError (
482+ "Invalid glob pattern for [project.license-files]: '{}'. {}" .format (pattern , ex .args [0 ])
483+ )
484+
485+ if not files and warn_no_files :
486+ raise ConfigError (
487+ "No files found for [project.license-files]: '{}' pattern" .format (pattern )
488+ )
489+ license_files .update (files )
490+ return license_files
491+
442492def _check_type (d , field_name , cls ):
443493 if not isinstance (d [field_name ], cls ):
444494 raise ConfigError (
@@ -533,6 +583,7 @@ def read_pep621_metadata(proj, path) -> LoadedConfig:
533583 if 'requires-python' in proj :
534584 md_dict ['requires_python' ] = proj ['requires-python' ]
535585
586+ license_files = set ()
536587 if 'license' in proj :
537588 _check_types (proj , 'license' , (str , dict ))
538589 if isinstance (proj ['license' ], str ):
@@ -554,14 +605,31 @@ def read_pep621_metadata(proj, path) -> LoadedConfig:
554605 )
555606 lc .referenced_files .append (license_tbl ['file' ])
556607 elif 'text' in license_tbl :
557- license = license_tbl ['text' ]
558- # TODO Normalize license if it's a valid SPDX expression
559- md_dict ['license' ] = license
608+ pass
560609 else :
561610 raise ConfigError (
562611 "file or text field required in [project.license] table"
563612 )
564613
614+ if 'license-files' in proj :
615+ _check_type (proj , 'license-files' , list )
616+ globs = proj ['license-files' ]
617+ license_files = _license_files_from_globs (path .parent , globs )
618+ if isinstance (proj .get ('license' ), dict ):
619+ raise ConfigError (
620+ "license-files cannot be used with a license table, "
621+ "use 'project.license' with a license expression instead"
622+ )
623+ else :
624+ license_files .update (
625+ _license_files_from_globs (
626+ path .parent , default_license_files_globs , warn_no_files = False
627+ )
628+ )
629+ license_files_sorted = sorted (license_files )
630+ lc .referenced_files .extend (license_files_sorted )
631+ md_dict ['license_files' ] = license_files_sorted
632+
565633 if 'authors' in proj :
566634 _check_type (proj , 'authors' , list )
567635 md_dict .update (pep621_people (proj ['authors' ]))
0 commit comments