|
6 | 6 | import os
|
7 | 7 | import re
|
8 | 8 | import sys
|
9 |
| -from collections.abc import Iterable, MutableMapping, Sequence |
10 |
| -from glob import iglob |
| 9 | +from collections.abc import Iterable, Iterator, MutableMapping, Sequence |
| 10 | +from glob import glob |
11 | 11 | from pathlib import Path
|
12 | 12 | from typing import TYPE_CHECKING, Any, Union
|
13 | 13 |
|
@@ -459,29 +459,79 @@ def _finalize_license_files(self) -> None:
|
459 | 459 | # See https://wheel.readthedocs.io/en/stable/user_guide.html
|
460 | 460 | # -> 'Including license files in the generated wheel file'
|
461 | 461 | patterns = ['LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*']
|
| 462 | + files = self._expand_patterns(patterns, enforce_match=False) |
| 463 | + else: # Patterns explicitly given by the user |
| 464 | + files = self._expand_patterns(patterns, enforce_match=True) |
462 | 465 |
|
463 |
| - self.metadata.license_files = list( |
464 |
| - map( |
465 |
| - lambda path: path.replace(os.sep, "/"), |
466 |
| - unique_everseen(self._expand_patterns(patterns)), |
467 |
| - ) |
468 |
| - ) |
| 466 | + self.metadata.license_files = list(unique_everseen(files)) |
469 | 467 |
|
470 |
| - @staticmethod |
471 |
| - def _expand_patterns(patterns): |
| 468 | + @classmethod |
| 469 | + def _expand_patterns( |
| 470 | + cls, patterns: list[str], enforce_match: bool = True |
| 471 | + ) -> Iterator[str]: |
472 | 472 | """
|
473 | 473 | >>> list(Distribution._expand_patterns(['LICENSE']))
|
474 | 474 | ['LICENSE']
|
475 | 475 | >>> list(Distribution._expand_patterns(['pyproject.toml', 'LIC*']))
|
476 | 476 | ['pyproject.toml', 'LICENSE']
|
| 477 | + >>> list(Distribution._expand_patterns(['setuptools/**/pyprojecttoml.py'])) |
| 478 | + ['setuptools/config/pyprojecttoml.py'] |
477 | 479 | """
|
478 | 480 | return (
|
479 |
| - path |
| 481 | + path.replace(os.sep, "/") |
480 | 482 | for pattern in patterns
|
481 |
| - for path in sorted(iglob(pattern, recursive=True)) |
| 483 | + for path in sorted(cls._find_pattern(pattern, enforce_match)) |
482 | 484 | if not path.endswith('~') and os.path.isfile(path)
|
483 | 485 | )
|
484 | 486 |
|
| 487 | + @staticmethod |
| 488 | + def _find_pattern(pattern: str, enforce_match: bool = True) -> list[str]: |
| 489 | + r""" |
| 490 | + >>> Distribution._find_pattern("LICENSE") |
| 491 | + ['LICENSE'] |
| 492 | + >>> Distribution._find_pattern("/LICENSE.MIT") |
| 493 | + Traceback (most recent call last): |
| 494 | + ... |
| 495 | + setuptools.errors.InvalidConfigError: Pattern '/LICENSE.MIT' should be relative... |
| 496 | + >>> Distribution._find_pattern("../LICENSE.MIT") |
| 497 | + Traceback (most recent call last): |
| 498 | + ... |
| 499 | + setuptools.errors.InvalidConfigError: ...Pattern '../LICENSE.MIT' cannot contain '..' |
| 500 | + >>> Distribution._find_pattern("LICEN{CSE*") |
| 501 | + Traceback (most recent call last): |
| 502 | + ... |
| 503 | + setuptools.warnings.SetuptoolsDeprecationWarning: ...Pattern 'LICEN{CSE*' contains invalid characters... |
| 504 | + """ |
| 505 | + if ".." in pattern: |
| 506 | + raise InvalidConfigError(f"Pattern {pattern!r} cannot contain '..'") |
| 507 | + if pattern.startswith((os.sep, "/")) or ":\\" in pattern: |
| 508 | + raise InvalidConfigError( |
| 509 | + f"Pattern {pattern!r} should be relative and must not start with '/'" |
| 510 | + ) |
| 511 | + if re.match(r'^[\w\-\.\/\*\?\[\]]+$', pattern) is None: |
| 512 | + pypa_guides = "specifications/pyproject-toml/#license-files" |
| 513 | + SetuptoolsDeprecationWarning.emit( |
| 514 | + "Please provide a valid glob pattern.", |
| 515 | + "Pattern {pattern!r} contains invalid characters.", |
| 516 | + pattern=pattern, |
| 517 | + see_url=f"https://packaging.python.org/en/latest/{pypa_guides}", |
| 518 | + due_date=(2026, 2, 20), # Introduced in 2025-02-20 |
| 519 | + ) |
| 520 | + |
| 521 | + found = glob(pattern, recursive=True) |
| 522 | + |
| 523 | + if enforce_match and not found: |
| 524 | + SetuptoolsDeprecationWarning.emit( |
| 525 | + "Cannot find any files for the given pattern.", |
| 526 | + "Pattern {pattern!r} did not match any files.", |
| 527 | + pattern=pattern, |
| 528 | + due_date=(2026, 2, 20), # Introduced in 2025-02-20 |
| 529 | + # PEP 639 requires us to error, but as a transition period |
| 530 | + # we will only issue a warning to give people time to prepare. |
| 531 | + # After the transition, this should raise an InvalidConfigError. |
| 532 | + ) |
| 533 | + return found |
| 534 | + |
485 | 535 | # FIXME: 'Distribution._parse_config_files' is too complex (14)
|
486 | 536 | def _parse_config_files(self, filenames=None): # noqa: C901
|
487 | 537 | """
|
|
0 commit comments