|
4 | 4 | """The module provides abstractions for the pypi package registry.""" |
5 | 5 | from __future__ import annotations |
6 | 6 |
|
| 7 | +import bisect |
7 | 8 | import hashlib |
8 | 9 | import logging |
9 | 10 | import os |
|
15 | 16 | import zipfile |
16 | 17 | from collections.abc import Callable, Generator, Iterator |
17 | 18 | from contextlib import contextmanager |
18 | | -from dataclasses import dataclass |
| 19 | +from dataclasses import dataclass, field |
19 | 20 | from datetime import datetime |
20 | 21 | from typing import TYPE_CHECKING |
21 | 22 |
|
@@ -502,6 +503,42 @@ def get_maintainer_join_date(self, username: str) -> datetime | None: |
502 | 503 |
|
503 | 504 | return res.replace(tzinfo=None) if res else None |
504 | 505 |
|
| 506 | + def get_matching_setuptools_version(self, package_release_datetime: datetime) -> str: |
| 507 | + """Find the setuptools that would be "latest" for the input datetime. |
| 508 | +
|
| 509 | + Parameters |
| 510 | + ---------- |
| 511 | + package_release_datetime: str |
| 512 | + Release datetime of a package we wish to rebuild |
| 513 | +
|
| 514 | + Returns |
| 515 | + ------- |
| 516 | + str: Matching version of setuptools |
| 517 | + """ |
| 518 | + setuptools_endpoint = urllib.parse.urljoin(self.registry_url, "pypi/setuptools/json") |
| 519 | + setuptools_json = self.download_package_json(setuptools_endpoint) |
| 520 | + releases = json_extract(setuptools_json, ["releases"], dict) |
| 521 | + if releases: |
| 522 | + release_tuples = [ |
| 523 | + (version, release_info[0].get("upload_time")) |
| 524 | + for version, release_info in releases.items() |
| 525 | + if release_info |
| 526 | + ] |
| 527 | + # Cannot assume this is sorted, as releases is just a dict |
| 528 | + release_tuples.sort(key=lambda x: x[1]) |
| 529 | + # bisect_left gives position to insert package_release_datetime to maintain order, hence we do -1 |
| 530 | + index = ( |
| 531 | + bisect.bisect_left( |
| 532 | + release_tuples, package_release_datetime, key=lambda x: datetime.strptime(x[1], "%Y-%m-%dT%H:%M:%S") |
| 533 | + ) |
| 534 | + - 1 |
| 535 | + ) |
| 536 | + return str(release_tuples[index][0]) |
| 537 | + # This realistically cannot happen: it would mean we somehow are trying to rebuild |
| 538 | + # for a package and version with no releases. |
| 539 | + # Return default just in case. |
| 540 | + return defaults.get("heuristic.pypi", "default_setuptools") |
| 541 | + |
505 | 542 | @staticmethod |
506 | 543 | def extract_attestation(attestation_data: dict) -> dict | None: |
507 | 544 | """Extract the first attestation file from a PyPI attestation response. |
@@ -618,13 +655,16 @@ class PyPIPackageJsonAsset: |
618 | 655 | package_json: dict |
619 | 656 |
|
620 | 657 | #: The source code temporary location name. |
621 | | - package_sourcecode_path: str |
| 658 | + package_sourcecode_path: str = field(init=False) |
622 | 659 |
|
623 | 660 | #: The wheel temporary location name. |
624 | | - wheel_path: str |
| 661 | + wheel_path: str = field(init=False) |
625 | 662 |
|
626 | 663 | #: Name of the wheel file. |
627 | | - wheel_filename: str |
| 664 | + wheel_filename: str = field(init=False) |
| 665 | + |
| 666 | + #: The datetime that the wheel was uploaded. |
| 667 | + wheel_upload_time: datetime = field(init=False) |
628 | 668 |
|
629 | 669 | #: The pypi inspector information about this package |
630 | 670 | inspector_asset: PyPIInspectorAsset |
@@ -779,6 +819,7 @@ def get_wheel_url(self, tag: str = "none-any") -> str | None: |
779 | 819 | # Continue to getting url |
780 | 820 | wheel_url: str = distribution.get("url") or "" |
781 | 821 | if wheel_url: |
| 822 | + self.wheel_upload_time = datetime.strptime(distribution.get("upload_time") or "", "%Y-%m-%dT%H:%M:%S") |
782 | 823 | try: |
783 | 824 | parsed_url = urllib.parse.urlparse(wheel_url) |
784 | 825 | except ValueError: |
@@ -919,6 +960,33 @@ def get_sourcecode_file_contents(self, path: str) -> bytes: |
919 | 960 | logger.debug(error_msg) |
920 | 961 | raise SourceCodeError(error_msg) from read_error |
921 | 962 |
|
| 963 | + def file_exists(self, path: str) -> bool: |
| 964 | + """Check if a file exists in the downloaded source code. |
| 965 | +
|
| 966 | + The path can be relative to the package_sourcecode_path attribute, or an absolute path. |
| 967 | +
|
| 968 | + Parameters |
| 969 | + ---------- |
| 970 | + path: str |
| 971 | + The absolute or relative to package_sourcecode_path file path to check for. |
| 972 | +
|
| 973 | + Returns |
| 974 | + ------- |
| 975 | + bool: Whether or not a file at path absolute or relative to package_sourcecode_path exists. |
| 976 | + """ |
| 977 | + if not self.package_sourcecode_path: |
| 978 | + # No source code files were downloaded |
| 979 | + return False |
| 980 | + |
| 981 | + if not os.path.isabs(path): |
| 982 | + path = os.path.join(self.package_sourcecode_path, path) |
| 983 | + |
| 984 | + if not os.path.exists(path): |
| 985 | + # Could not find a file at that path |
| 986 | + return False |
| 987 | + |
| 988 | + return True |
| 989 | + |
922 | 990 | def iter_sourcecode(self) -> Iterator[tuple[str, bytes]]: |
923 | 991 | """ |
924 | 992 | Iterate through all source code files. |
@@ -1054,6 +1122,16 @@ def get_inspector_src_preview_links(self) -> bool: |
1054 | 1122 | # If all distributions were invalid and went along a 'continue' path. |
1055 | 1123 | return bool(self.inspector_asset) |
1056 | 1124 |
|
| 1125 | + def get_chronologically_suitable_setuptools_version(self) -> str: |
| 1126 | + """Find version of setuptools that would be "latest" for this package. |
| 1127 | +
|
| 1128 | + Returns |
| 1129 | + ------- |
| 1130 | + str |
| 1131 | + Chronologically likeliest setuptools version |
| 1132 | + """ |
| 1133 | + return self.pypi_registry.get_matching_setuptools_version(self.wheel_upload_time) |
| 1134 | + |
1057 | 1135 |
|
1058 | 1136 | def find_or_create_pypi_asset( |
1059 | 1137 | asset_name: str, asset_version: str | None, pypi_registry_info: PackageRegistryInfo |
@@ -1091,8 +1169,6 @@ def find_or_create_pypi_asset( |
1091 | 1169 | logger.debug("Failed to create PyPIPackageJson asset.") |
1092 | 1170 | return None |
1093 | 1171 |
|
1094 | | - asset = PyPIPackageJsonAsset( |
1095 | | - asset_name, asset_version, False, package_registry, {}, "", "", "", PyPIInspectorAsset("", [], {}) |
1096 | | - ) |
| 1172 | + asset = PyPIPackageJsonAsset(asset_name, asset_version, False, package_registry, {}, PyPIInspectorAsset("", [], {})) |
1097 | 1173 | pypi_registry_info.metadata.append(asset) |
1098 | 1174 | return asset |
0 commit comments