From 85a80350864f0322d7b299a35931e85ccb269775 Mon Sep 17 00:00:00 2001 From: Maxwell G Date: Thu, 12 Jan 2023 14:09:58 -0600 Subject: [PATCH] Add more robust license file installation - Expand default license file inclusion globs. This uses the same patterns as setuptools/wheel with the addition of 'LICENSES/*.txt' for REUSE. - Ensure matching license files are always included in sdists. - Warn users when no license files are detected. - Add unit tests --- flit_core/flit_core/config.py | 20 ++++++++++++++++++-- flit_core/flit_core/sdist.py | 3 ++- flit_core/flit_core/tests/test_config.py | 17 +++++++++++++++++ flit_core/flit_core/wheel.py | 13 +++++++------ 4 files changed, 44 insertions(+), 9 deletions(-) diff --git a/flit_core/flit_core/config.py b/flit_core/flit_core/config.py index 12929561..21f545c2 100644 --- a/flit_core/flit_core/config.py +++ b/flit_core/flit_core/config.py @@ -21,6 +21,9 @@ log = logging.getLogger(__name__) +# These are the same patterns used by setuptools/wheel +# besides `LICENSES/*.txt` which is from the REUSE specification +LICENSE_PATTERNS = ('AUTHORS*', 'COPYING*', 'LICEN[CS]E*', 'LICENSES/*.txt') class ConfigError(ValueError): pass @@ -257,6 +260,7 @@ def __init__(self): self.sdist_exclude_patterns = [] self.dynamic_metadata = [] self.data_directory = None + self.license_files = [] def add_scripts(self, scripts_dict): if scripts_dict: @@ -401,6 +405,8 @@ def _prep_metadata(md_sect, path): # For internal use, record the main requirements as a '.none' extra. res.reqs_by_extra['.none'] = reqs_noextra + res.license_files = find_licenses(path.parent if path else Path(".")) + return res def _expand_requires_extra(re): @@ -506,7 +512,7 @@ def read_pep621_metadata(proj, path) -> LoadedConfig: "Unrecognised keys in [project.license]: {}".format(unrec_keys) ) - # TODO: Do something with license info. + # TODO: Include license info in the metadata. # The 'License' field in packaging metadata is a brief description of # a license, not the full text or a file path. PEP 639 will improve on # how licenses are recorded. @@ -515,13 +521,15 @@ def read_pep621_metadata(proj, path) -> LoadedConfig: raise ConfigError( "[project.license] should specify file or text, not both" ) - lc.referenced_files.append(license_tbl['file']) + lc.license_files.append(Path(license_tbl['file'])) elif 'text' in license_tbl: pass else: raise ConfigError( "file or text field required in [project.license] table" ) + if not lc.license_files: + lc.license_files = find_licenses(path.parent) if 'authors' in proj: _check_type(proj, 'authors', list) @@ -658,3 +666,11 @@ def pep621_people(people, group_name='author') -> dict: if emails: res[group_name + '_email'] = ", ".join(emails) return res + +def find_licenses(path): + found = [] + for pattern in LICENSE_PATTERNS: + found.extend(file for file in path.glob(pattern) if file.is_file()) + if not found: + log.warning("No licenses were found in %s. Add a license!", LICENSE_PATTERNS) + return found diff --git a/flit_core/flit_core/sdist.py b/flit_core/flit_core/sdist.py index f41d177f..45d418ef 100644 --- a/flit_core/flit_core/sdist.py +++ b/flit_core/flit_core/sdist.py @@ -91,7 +91,8 @@ def from_ini_path(cls, ini_path: Path): srcdir = ini_path.parent module = common.Module(ini_info.module, srcdir) metadata = common.make_metadata(module, ini_info) - extra_files = [ini_path.name] + ini_info.referenced_files + license_files = list(map(str, ini_info.license_files)) + extra_files = [ini_path.name] + ini_info.referenced_files + license_files return cls( module, metadata, srcdir, ini_info.reqs_by_extra, ini_info.entrypoints, extra_files, ini_info.data_directory, diff --git a/flit_core/flit_core/tests/test_config.py b/flit_core/flit_core/tests/test_config.py index eafb7e99..7d0414cd 100644 --- a/flit_core/flit_core/tests/test_config.py +++ b/flit_core/flit_core/tests/test_config.py @@ -163,3 +163,20 @@ def test_bad_pep621_readme(readme, err_match): } with pytest.raises(config.ConfigError, match=err_match): config.read_pep621_metadata(proj, samples_dir / 'pep621') + +def test_license_file_auto_detect(tmp_path): + proj = {'name': 'module1', 'version': '1.0', 'description': 'x'} + tmp_path.joinpath('module1').mkdir() + for file in ('LICENSE', 'COPYING.md', 'module1/__init__.py'): + tmp_path.joinpath(file).touch() + parsed_config = config.read_pep621_metadata(proj, tmp_path / "pyproject.toml") + assert parsed_config.license_files == [tmp_path / 'COPYING.md', tmp_path / 'LICENSE'] + +def test_missing_license_warning(tmp_path, caplog): + proj = {'name': 'module1', 'version': '1.0', 'description': 'x'} + tmp_path.joinpath('module1').mkdir() + for file in ('module1/__init__.py',): + tmp_path.joinpath(file).touch() + config.read_pep621_metadata(proj, tmp_path / 'pyproject.toml') + message = f"No licenses were found in {config.LICENSE_PATTERNS}. Add a license!" + assert message in caplog.messages diff --git a/flit_core/flit_core/wheel.py b/flit_core/flit_core/wheel.py index 08cb70ae..c10cda83 100644 --- a/flit_core/flit_core/wheel.py +++ b/flit_core/flit_core/wheel.py @@ -60,7 +60,8 @@ def zip_timestamp_from_env() -> Optional[tuple]: class WheelBuilder: def __init__( - self, directory, module, metadata, entrypoints, target_fp, data_directory + self, directory, module, metadata, entrypoints, target_fp, data_directory, + license_files ): """Build a wheel from a module/package """ @@ -69,6 +70,7 @@ def __init__( self.metadata = metadata self.entrypoints = entrypoints self.data_directory = data_directory + self.license_files = license_files self.records = [] self.source_time_stamp = zip_timestamp_from_env() @@ -86,7 +88,8 @@ def from_ini_path(cls, ini_path, target_fp): module = common.Module(ini_info.module, directory) metadata = common.make_metadata(module, ini_info) return cls( - directory, module, metadata, entrypoints, target_fp, ini_info.data_directory + directory, module, metadata, entrypoints, target_fp, ini_info.data_directory, + ini_info.license_files ) @property @@ -181,10 +184,8 @@ def write_metadata(self): with self._write_to_zip(self.dist_info + '/entry_points.txt') as f: common.write_entry_points(self.entrypoints, f) - for base in ('COPYING', 'LICENSE'): - for path in sorted(self.directory.glob(base + '*')): - if path.is_file(): - self._add_file(path, '%s/%s' % (self.dist_info, path.name)) + for path in self.license_files: + self._add_file(path, '%s/%s' % (self.dist_info, path.name)) with self._write_to_zip(self.dist_info + '/WHEEL') as f: _write_wheel_file(f, supports_py2=self.metadata.supports_py2)