|
6 | 6 | import pathlib |
7 | 7 | import tempfile |
8 | 8 | import typing |
| 9 | +import zipfile |
9 | 10 |
|
10 | | -import pkginfo |
11 | 11 | import pyproject_hooks |
12 | 12 | import tomlkit |
13 | 13 | from packaging.metadata import Metadata |
14 | 14 | from packaging.requirements import Requirement |
15 | | -from packaging.utils import NormalizedName, canonicalize_name |
| 15 | +from packaging.utils import NormalizedName, canonicalize_name, parse_wheel_filename |
16 | 16 | from packaging.version import Version |
17 | 17 |
|
18 | 18 | from . import ( |
@@ -344,14 +344,23 @@ def default_get_install_dependencies_of_sdist( |
344 | 344 | return set(metadata.requires_dist) |
345 | 345 |
|
346 | 346 |
|
347 | | -def parse_metadata(metadata_file: pathlib.Path, *, validate: bool = True) -> Metadata: |
348 | | - """Parse a dist-info/METADATA file |
| 347 | +def parse_metadata( |
| 348 | + metadata_source: pathlib.Path | bytes, *, validate: bool = True |
| 349 | +) -> Metadata: |
| 350 | + """Parse metadata from a file path or bytes. |
| 351 | +
|
| 352 | + Args: |
| 353 | + metadata_source: Path to METADATA file or bytes containing metadata |
| 354 | + validate: Whether to validate metadata (default: True) |
349 | 355 |
|
350 | | - The default parse mode is 'strict'. It even fails for a mismatch of field |
351 | | - and core metadata version, e.g. a package with metadata 2.2 and |
352 | | - license-expression field (added in 2.4). |
| 356 | + Returns: |
| 357 | + Parsed Metadata object |
353 | 358 | """ |
354 | | - return Metadata.from_email(metadata_file.read_bytes(), validate=validate) |
| 359 | + if isinstance(metadata_source, pathlib.Path): |
| 360 | + metadata_bytes = metadata_source.read_bytes() |
| 361 | + else: |
| 362 | + metadata_bytes = metadata_source |
| 363 | + return Metadata.from_email(metadata_bytes, validate=validate) |
355 | 364 |
|
356 | 365 |
|
357 | 366 | def pep517_metadata_of_sdist( |
@@ -418,16 +427,65 @@ def validate_dist_name_version( |
418 | 427 | def get_install_dependencies_of_wheel( |
419 | 428 | req: Requirement, wheel_filename: pathlib.Path, requirements_file_dir: pathlib.Path |
420 | 429 | ) -> set[Requirement]: |
| 430 | + """Get install dependencies from a wheel file. |
| 431 | +
|
| 432 | + Extracts and parses the METADATA file from the wheel to get the |
| 433 | + Requires-Dist entries. |
| 434 | +
|
| 435 | + Args: |
| 436 | + req: The requirement being processed |
| 437 | + wheel_filename: Path to the wheel file |
| 438 | + requirements_file_dir: Directory to write the requirements file |
| 439 | +
|
| 440 | + Returns: |
| 441 | + Set of requirements from the wheel's metadata |
| 442 | + """ |
421 | 443 | logger.info(f"getting installation dependencies from {wheel_filename}") |
422 | | - wheel = pkginfo.Wheel(str(wheel_filename)) |
423 | | - deps = _filter_requirements(req, wheel.requires_dist) |
| 444 | + # Disable validation because many third-party packages have metadata version |
| 445 | + # mismatches (e.g., setuptools declares Metadata-Version: 2.2 but uses |
| 446 | + # license-file which was introduced in 2.4). The old pkginfo library |
| 447 | + # didn't validate this, so we maintain backward compatibility. |
| 448 | + metadata = _get_metadata_from_wheel(wheel_filename, validate=False) |
| 449 | + requires_dist = metadata.requires_dist or [] |
| 450 | + deps = _filter_requirements(req, requires_dist) |
424 | 451 | _write_requirements_file( |
425 | 452 | deps, |
426 | 453 | requirements_file_dir / INSTALL_REQ_FILE_NAME, |
427 | 454 | ) |
428 | 455 | return deps |
429 | 456 |
|
430 | 457 |
|
| 458 | +def _get_metadata_from_wheel( |
| 459 | + wheel_filename: pathlib.Path, *, validate: bool = True |
| 460 | +) -> Metadata: |
| 461 | + """Extract and parse METADATA from a wheel file. |
| 462 | +
|
| 463 | + Args: |
| 464 | + wheel_filename: Path to the wheel file |
| 465 | + validate: Whether to validate metadata (default: True) |
| 466 | +
|
| 467 | + Returns: |
| 468 | + Parsed Metadata object |
| 469 | +
|
| 470 | + Raises: |
| 471 | + ValueError: If no METADATA file is found in the wheel |
| 472 | + """ |
| 473 | + # Get dist-info path from wheel filename. |
| 474 | + # Uses same pattern as wheels.extract_info_from_wheel_file: |
| 475 | + _, dist_version, _, _ = parse_wheel_filename(wheel_filename.name) |
| 476 | + dist_name = wheel_filename.name.split("-", 1)[0] |
| 477 | + metadata_path = f"{dist_name}-{dist_version}.dist-info/METADATA" |
| 478 | + |
| 479 | + with zipfile.ZipFile(wheel_filename) as whl: |
| 480 | + try: |
| 481 | + metadata_content = whl.read(metadata_path) |
| 482 | + except KeyError: |
| 483 | + raise ValueError( |
| 484 | + f"Could not find METADATA file in wheel: {wheel_filename}" |
| 485 | + ) from None |
| 486 | + return parse_metadata(metadata_content, validate=validate) |
| 487 | + |
| 488 | + |
431 | 489 | def get_pyproject_contents(sdist_root_dir: pathlib.Path) -> dict[str, typing.Any]: |
432 | 490 | pyproject_toml_filename = sdist_root_dir / "pyproject.toml" |
433 | 491 | if not os.path.exists(pyproject_toml_filename): |
|
0 commit comments