Skip to content

Commit 90afccf

Browse files
Replaceing the Python email library parser from packaging.metadata
Replaces the Python email library parser with packaging.metadata.Metadata for parsing wheel/package metadata. Fixes: #561 Signed-off-by: Lalatendu Mohanty <lmohanty@redhat.com>
1 parent 3ac42dc commit 90afccf

File tree

4 files changed

+34
-36
lines changed

4 files changed

+34
-36
lines changed

src/fromager/bootstrapper.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import tempfile
1111
import typing
1212
import zipfile
13-
from email.parser import BytesParser
1413
from urllib.parse import urlparse
1514

1615
from packaging.requirements import Requirement
@@ -907,10 +906,8 @@ def _get_version_from_package_metadata(
907906
config_settings=pbi.config_settings,
908907
)
909908
metadata_filename = source_dir.parent / metadata_dir_base / "METADATA"
910-
with open(metadata_filename, "rb") as f:
911-
p = BytesParser()
912-
metadata = p.parse(f, headersonly=True)
913-
return Version(metadata["Version"])
909+
metadata = dependencies.parse_metadata(metadata_filename)
910+
return metadata.version
914911

915912
def _resolve_prebuilt_with_history(
916913
self,

src/fromager/candidate.py

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,19 @@
22
import datetime
33
import logging
44
import typing
5-
from email.message import EmailMessage, Message
6-
from email.parser import BytesParser
75
from io import BytesIO
8-
from typing import TYPE_CHECKING
96
from zipfile import ZipFile
107

8+
from packaging.metadata import Metadata
119
from packaging.requirements import Requirement
1210
from packaging.utils import BuildTag, canonicalize_name
1311
from packaging.version import Version
1412

13+
from . import dependencies
1514
from .request_session import session
1615

1716
logger = logging.getLogger(__name__)
1817

19-
# fix for runtime errors caused by inheriting classes that are generic in stubs but not runtime
20-
# https://mypy.readthedocs.io/en/latest/runtime_troubles.html#using-classes-that-are-generic-in-stubs-but-not-at-runtime
21-
if TYPE_CHECKING:
22-
Metadata = Message[str, str]
23-
else:
24-
Metadata = Message
25-
2618

2719
@dataclasses.dataclass(frozen=True, order=True, slots=True, repr=False, kw_only=True)
2820
class Candidate:
@@ -73,11 +65,10 @@ def metadata(self) -> Metadata:
7365
return self._metadata
7466

7567
def _get_dependencies(self) -> typing.Iterable[Requirement]:
76-
deps = self.metadata.get_all("Requires-Dist", [])
68+
deps = self.metadata.requires_dist or []
7769
extras = self.extras if self.extras else [""]
7870

79-
for d in deps:
80-
r = Requirement(d)
71+
for r in deps:
8172
if r.marker is None:
8273
yield r
8374
else:
@@ -95,7 +86,8 @@ def dependencies(self) -> list[Requirement]:
9586

9687
@property
9788
def requires_python(self) -> str | None:
98-
return self.metadata.get("Requires-Python")
89+
spec = self.metadata.requires_python
90+
return str(spec) if spec is not None else None
9991

10092

10193
def get_metadata_for_wheel(url: str, metadata_url: str | None = None) -> Metadata:
@@ -107,7 +99,7 @@ def get_metadata_for_wheel(url: str, metadata_url: str | None = None) -> Metadat
10799
metadata_url: Optional URL of the metadata file (PEP 658)
108100
109101
Returns:
110-
Parsed metadata as a Message object
102+
Parsed metadata as a Metadata object
111103
"""
112104
# Try PEP 658 metadata endpoint first if available
113105
if metadata_url:
@@ -119,8 +111,7 @@ def get_metadata_for_wheel(url: str, metadata_url: str | None = None) -> Metadat
119111
response.raise_for_status()
120112

121113
# Parse metadata directly from the response content
122-
p = BytesParser()
123-
metadata = p.parse(BytesIO(response.content), headersonly=True)
114+
metadata = dependencies.parse_metadata(response.content)
124115
logger.debug(f"Successfully retrieved metadata via PEP 658 for {url}")
125116
return metadata
126117

@@ -136,8 +127,8 @@ def get_metadata_for_wheel(url: str, metadata_url: str | None = None) -> Metadat
136127
with ZipFile(BytesIO(data)) as z:
137128
for n in z.namelist():
138129
if n.endswith(".dist-info/METADATA"):
139-
p = BytesParser()
140-
return p.parse(z.open(n), headersonly=True)
130+
metadata_content = z.read(n)
131+
return dependencies.parse_metadata(metadata_content)
141132

142-
# If we didn't find the metadata, return an empty dict
143-
return EmailMessage()
133+
# If we didn't find the metadata, raise an error
134+
raise ValueError(f"Could not find METADATA file in wheel: {url}")

src/fromager/dependencies.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -344,14 +344,23 @@ def default_get_install_dependencies_of_sdist(
344344
return set(metadata.requires_dist)
345345

346346

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)
349355
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
353358
"""
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)
355364

356365

357366
def pep517_metadata_of_sdist(

tests/test_pep658_support.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,11 @@ def test_get_metadata_with_pep658_success(self, mock_session) -> None:
5656
metadata = get_metadata_for_wheel(wheel_url, metadata_url)
5757

5858
# Verify the metadata was parsed correctly
59-
assert metadata["Name"] == "test-package"
60-
assert metadata["Version"] == "1.0.0"
61-
assert metadata["Summary"] == "A test package"
62-
assert "requests >= 2.0.0" in metadata.get_all("Requires-Dist", [])
59+
assert metadata.name == "test-package"
60+
assert str(metadata.version) == "1.0.0"
61+
assert metadata.summary == "A test package"
62+
assert metadata.requires_dist is not None
63+
assert any(str(req) == "requests>=2.0.0" for req in metadata.requires_dist)
6364

6465
# Verify only the metadata URL was called, not the wheel URL
6566
mock_session.get.assert_called_once_with(metadata_url)

0 commit comments

Comments
 (0)