Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial warnings regarding license-files glob patterns. #4838

Open
wants to merge 6 commits into
base: feature/pep639
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions newsfragments/4838.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Added simple validation for given glob patterns in ``license-files``:
a warning will be generated if no file is matched.
Invalid glob patterns can raise an exception.
74 changes: 62 additions & 12 deletions setuptools/dist.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
import os
import re
import sys
from collections.abc import Iterable, MutableMapping, Sequence
from glob import iglob
from collections.abc import Iterable, Iterator, MutableMapping, Sequence
from glob import glob
from pathlib import Path
from typing import TYPE_CHECKING, Any, Union

Expand Down Expand Up @@ -459,29 +459,79 @@ def _finalize_license_files(self) -> None:
# See https://wheel.readthedocs.io/en/stable/user_guide.html
# -> 'Including license files in the generated wheel file'
patterns = ['LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*']
files = self._expand_patterns(patterns, enforce_match=False)
else: # Patterns explicitly given by the user
files = self._expand_patterns(patterns, enforce_match=True)

self.metadata.license_files = list(
map(
lambda path: path.replace(os.sep, "/"),
unique_everseen(self._expand_patterns(patterns)),
)
)
self.metadata.license_files = list(unique_everseen(files))

@staticmethod
def _expand_patterns(patterns):
@classmethod
def _expand_patterns(
cls, patterns: list[str], enforce_match: bool = True
) -> Iterator[str]:
"""
>>> list(Distribution._expand_patterns(['LICENSE']))
['LICENSE']
>>> list(Distribution._expand_patterns(['pyproject.toml', 'LIC*']))
['pyproject.toml', 'LICENSE']
>>> list(Distribution._expand_patterns(['setuptools/**/pyprojecttoml.py']))
['setuptools/config/pyprojecttoml.py']
"""
return (
path
path.replace(os.sep, "/")
for pattern in patterns
for path in sorted(iglob(pattern, recursive=True))
for path in sorted(cls._find_pattern(pattern, enforce_match))
if not path.endswith('~') and os.path.isfile(path)
)

@staticmethod
def _find_pattern(pattern: str, enforce_match: bool = True) -> list[str]:
r"""
>>> Distribution._find_pattern("LICENSE")
['LICENSE']
>>> Distribution._find_pattern("/LICENSE.MIT")
Traceback (most recent call last):
...
setuptools.errors.InvalidConfigError: Pattern '/LICENSE.MIT' should be relative...
>>> Distribution._find_pattern("../LICENSE.MIT")
Traceback (most recent call last):
...
setuptools.errors.InvalidConfigError: ...Pattern '../LICENSE.MIT' cannot contain '..'
>>> Distribution._find_pattern("LICEN{CSE*")
Traceback (most recent call last):
...
setuptools.warnings.SetuptoolsDeprecationWarning: ...Pattern 'LICEN{CSE*' contains invalid characters...
"""
if ".." in pattern:
raise InvalidConfigError(f"Pattern {pattern!r} cannot contain '..'")
if pattern.startswith((os.sep, "/")) or ":\\" in pattern:
raise InvalidConfigError(
f"Pattern {pattern!r} should be relative and must not start with '/'"
)
if re.match(r'^[\w\-\.\/\*\?\[\]]+$', pattern) is None:
pypa_guides = "specifications/pyproject-toml/#license-files"
SetuptoolsDeprecationWarning.emit(
"Please provide a valid glob pattern.",
"Pattern {pattern!r} contains invalid characters.",
pattern=pattern,
see_url=f"https://packaging.python.org/en/latest/{pypa_guides}",
due_date=(2025, 2, 20), # Introduced in 2024-02-20
)

found = glob(pattern, recursive=True)

if enforce_match and not found:
SetuptoolsDeprecationWarning.emit(
"Cannot find any files for the given pattern.",
"Pattern {pattern!r} did not match any files.",
pattern=pattern,
due_date=(2025, 2, 20), # Introduced in 2024-02-20
# PEP 639 requires us to error, but as a transition period
# we will only issue a warning to give people time to prepare.
# After the transition, this should raise an InvalidConfigError.
)
return found

# FIXME: 'Distribution._parse_config_files' is too complex (14)
def _parse_config_files(self, filenames=None): # noqa: C901
"""
Expand Down
8 changes: 8 additions & 0 deletions setuptools/tests/config/test_apply_pyprojecttoml.py
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,14 @@ def test_default_patterns(self, tmp_path):
assert (tmp_path / "LICENSE.txt").exists() # from base example
assert set(dist.metadata.license_files) == {*license_files, "LICENSE.txt"}

def test_missing_patterns(self, tmp_path):
pyproject = self.base_pyproject_license_pep639(tmp_path)
assert list(tmp_path.glob("_FILE*")) == [] # sanity check

msg = "Cannot find any files for the given pattern.*"
with pytest.warns(SetuptoolsDeprecationWarning, match=msg):
pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)

def test_deprecated_file_expands_to_text(self, tmp_path):
"""Make sure the old example with ``license = {text = ...}`` works"""

Expand Down
3 changes: 3 additions & 0 deletions setuptools/tests/test_core_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,9 @@ def dist(self, request, monkeypatch, tmp_path):
monkeypatch.chdir(tmp_path)
monkeypatch.setattr(expand, "read_attr", Mock(return_value="0.42"))
monkeypatch.setattr(expand, "read_files", Mock(return_value="hello world"))
monkeypatch.setattr(
Distribution, "_finalize_license_files", Mock(return_value=None)
)
if request.param is None:
yield self.base_example()
else:
Expand Down
Loading