Skip to content

Commit

Permalink
Add initial support for license-files
Browse files Browse the repository at this point in the history
  • Loading branch information
cdce8p committed Nov 20, 2024
1 parent 7cc7d33 commit e6ccab1
Show file tree
Hide file tree
Showing 13 changed files with 165 additions and 5 deletions.
2 changes: 2 additions & 0 deletions doc/pyproject_toml.rst
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ requires-python
license
A table with either a ``file`` key (a relative path to a license file) or a
``text`` key (the license text).
license-files
A list of glob patterns for license files to include. Defaults to ``['COPYING*', 'LICENSE*']``.
authors
A list of tables with ``name`` and ``email`` keys (both optional) describing
the authors of the project.
Expand Down
5 changes: 5 additions & 0 deletions flit_core/flit_core/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ class Metadata(object):
obsoletes_dist = ()
requires_external = ()
provides_extra = ()
license_files = ()
dynamic = ()

metadata_version = "2.3"
Expand Down Expand Up @@ -425,6 +426,10 @@ def write_metadata_file(self, fp):
for clsfr in self.classifiers:
fp.write(u'Classifier: {}\n'.format(clsfr))

# TODO: License-File requires Metadata-Version '2.4'
# for file in self.license_files:
# fp.write(u'License-File: {}\n'.format(file))

for req in self.requires_dist:
normalised_req = self._normalise_requires_dist(req)
fp.write(u'Requires-Dist: {}\n'.format(normalised_req))
Expand Down
57 changes: 57 additions & 0 deletions flit_core/flit_core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ class ConfigError(ValueError):
'readme',
'requires-python',
'license',
'license-files',
'authors',
'maintainers',
'keywords',
Expand All @@ -73,6 +74,8 @@ class ConfigError(ValueError):
'dynamic',
}

default_license_files_globs = ['COPYING*', 'LICENSE*']


def read_flit_config(path):
"""Read and check the `pyproject.toml` file with data about the package.
Expand Down Expand Up @@ -427,6 +430,13 @@ def _prep_metadata(md_sect, path):
# For internal use, record the main requirements as a '.none' extra.
res.reqs_by_extra['.none'] = reqs_noextra

if path:
license_files = _license_files_from_globs(
path.parent, default_license_files_globs, use_defaults=True
)
res.referenced_files.extend(license_files)
md_dict['license_files'] = license_files

return res

def _expand_requires_extra(re):
Expand All @@ -439,6 +449,41 @@ def _expand_requires_extra(re):
yield '{} ; extra == "{}"'.format(req, extra)


def _license_files_from_globs(project_dir: Path, globs: list[str], use_defaults: bool = False) -> list[str]:
license_files: list[Path] = []
for pattern in globs:
if pattern.startswith("/"):
raise ConfigError(
"Invalid glob pattern for [project.license-files]: '{}'. "
"Pattern must not start with '/'.".format(pattern)
)
if ".." in pattern:
raise ConfigError(
"Invalid glob pattern for [project.license-files]: '{}'. "
"Pattern must not contain '..'".format(pattern)
)
try:
files = list(
filter(
lambda file: file.is_file(),
map(
lambda file: file.relative_to(project_dir),
project_dir.glob(pattern)
)
)
)
except ValueError as ex:
raise ConfigError(
"Invalid glob pattern for [project.license-files]: '{}'. {}".format(pattern, ex.args[0])
)

if not files and not use_defaults:
raise ConfigError(
"No files found for [project.license-files]: '{}' pattern".format(pattern)
)
license_files.extend(files)
return sorted(map(str, license_files))

def _check_type(d, field_name, cls):
if not isinstance(d[field_name], cls):
raise ConfigError(
Expand Down Expand Up @@ -551,6 +596,18 @@ def read_pep621_metadata(proj, path) -> LoadedConfig:
"file or text field required in [project.license] table"
)

license_files = []
if 'license-files' in proj:
_check_type(proj, 'license-files', list)
globs = proj['license-files']
license_files = _license_files_from_globs(path.parent, globs)
else:
license_files = _license_files_from_globs(
path.parent, default_license_files_globs, use_defaults=True
)
lc.referenced_files.extend(license_files)
md_dict['license_files'] = license_files

if 'authors' in proj:
_check_type(proj, 'authors', list)
md_dict.update(pep621_people(proj['authors']))
Expand Down
6 changes: 2 additions & 4 deletions flit_core/flit_core/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,10 +183,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 file in self.metadata.license_files:
self._add_file(self.directory / file, '%s/licenses/%s' % (self.dist_info, file))

with self._write_to_zip(self.dist_info + '/WHEEL') as f:
_write_wheel_file(f, supports_py2=self.metadata.supports_py2)
Expand Down
1 change: 1 addition & 0 deletions flit_core/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ dependencies = []
requires-python = '>=3.6'
readme = "README.rst"
license = {file = "LICENSE"}
license-files = ["LICENSE*", "flit_core/vendor/**/LICENSE*"]
classifiers = [
"License :: OSI Approved :: BSD License",
"Topic :: Software Development :: Libraries :: Python Modules",
Expand Down
1 change: 1 addition & 0 deletions flit_core/tests_core/samples/pep621_license_files/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This file should be added to wheels
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Readme
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This file should be added to wheels
3 changes: 3 additions & 0 deletions flit_core/tests_core/samples/pep621_license_files/module1a.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Example module"""

__version__ = '0.1'
39 changes: 39 additions & 0 deletions flit_core/tests_core/samples/pep621_license_files/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
[build-system]
requires = ["flit_core >=3.2,<4"]
build-backend = "flit_core.buildapi"

[project]
name = "module1"
authors = [
{name = "Sir Röbin", email = "[email protected]"}
]
maintainers = [
{name = "Sir Galahad"}
]
readme = "README.rst"
license-files = ["**/LICENSE*"]
requires-python = ">=3.7"
dependencies = [
"requests >= 2.18",
"docutils",
]
keywords = ["example", "test"]
dynamic = [
"version",
"description",
]

[project.optional-dependencies]
test = [
"pytest",
"mock; python_version<'3.6'"
]

[project.urls]
homepage = "http://github.com/sirrobin/module1"

[project.entry-points.flit_test_example]
foo = "module1:main"

[tool.flit.module]
name = "module1a"
38 changes: 38 additions & 0 deletions flit_core/tests_core/test_config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import logging
import re
import os
from pathlib import Path
from unittest.mock import patch
import pytest

from flit_core import config
Expand Down Expand Up @@ -178,3 +181,38 @@ def test_bad_pep621_readme(readme, err_match):
}
with pytest.raises(config.ConfigError, match=err_match):
config.read_pep621_metadata(proj, samples_dir / 'pep621')


@pytest.mark.parametrize(('value', 'files'), [
('[]', []),
('["LICENSE"]', ["LICENSE"]),
('["LICENSE*"]', ["LICENSE"]),
('["**/LICENSE*"]', ["LICENSE", "module/vendor/LICENSE_VENDOR"]),
('["module/vendor/LICENSE*"]', ["module/vendor/LICENSE_VENDOR"]),
('["LICENSE", "module/**/LICENSE*"]', ["LICENSE", "module/vendor/LICENSE_VENDOR"]),
])
def test_pep621_license_files(value, files):
path = samples_dir / 'pep621_license_files' / 'pyproject.toml'
data = path.read_text()
data = re.sub(r"(^license-files = )(?:\[.*\])", r"\g<1>{}".format(value), data, 1, flags=re.M)
dir = os.getcwd()
try:
os.chdir(samples_dir / 'pep621_license_files')
with patch("pathlib.Path.read_text", return_value=data):
info = config.read_flit_config(path)
assert info.metadata['license_files'] == files
finally:
os.chdir(dir)


@pytest.mark.parametrize(('proj_bad', 'err_match'), [
({'license-files': ["/LICENSE"]}, "'/LICENSE'.+must not start with '/'"),
({'license-files': ["../LICENSE"]}, "'../LICENSE'.+must not contain '..'"),
({'license-files': ["**LICENSE"]}, "'\\*\\*LICENSE'.+Invalid pattern"),
({'license-files': ["NOT_FOUND"]}, "No files found.+'NOT_FOUND'"),
])
def test_bad_pep621_license_files(proj_bad, err_match):
proj = {'name': 'module1', 'version': '1.0', 'description': 'x'}
proj.update(proj_bad)
with pytest.raises(config.ConfigError, match=err_match):
config.read_pep621_metadata(proj, samples_dir / 'pep621')
14 changes: 14 additions & 0 deletions flit_core/tests_core/test_wheel.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
from pathlib import Path
from zipfile import ZipFile

Expand Down Expand Up @@ -45,3 +46,16 @@ def test_data_dir(tmp_path):
assert_isfile(info.file)
with ZipFile(info.file, 'r') as zf:
assert 'module1-0.1.data/data/share/man/man1/foo.1' in zf.namelist()


def test_license_files(tmp_path):
dir = os.getcwd()
try:
os.chdir(samples_dir / 'pep621_license_files')
info = make_wheel_in(samples_dir / 'pep621_license_files' / 'pyproject.toml', tmp_path)
assert_isfile(info.file)
with ZipFile(info.file, 'r') as zf:
assert 'module1-0.1.dist-info/licenses/LICENSE' in zf.namelist()
assert 'module1-0.1.dist-info/licenses/module/vendor/LICENSE_VENDOR' in zf.namelist()
finally:
os.chdir(dir)
2 changes: 1 addition & 1 deletion tests/test_wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def test_wheel_src_module(copy_sample):
with unpack(whl_file) as unpacked:
assert_isfile(Path(unpacked, 'module3.py'))
assert_isdir(Path(unpacked, 'module3-0.1.dist-info'))
assert_isfile(Path(unpacked, 'module3-0.1.dist-info', 'LICENSE'))
assert_isfile(Path(unpacked, 'module3-0.1.dist-info', 'licenses', 'LICENSE'))

def test_editable_wheel_src_module(copy_sample):
td = copy_sample('module3')
Expand Down

0 comments on commit e6ccab1

Please sign in to comment.