Skip to content

Commit c1b5f21

Browse files
committed
publish: use the artifact metadata instead of the project metadata
Especially, since we do support other build backends than poetry-core this is more correct.
1 parent 4e181a1 commit c1b5f21

File tree

2 files changed

+142
-52
lines changed

2 files changed

+142
-52
lines changed

src/poetry/publishing/uploader.py

Lines changed: 67 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
from __future__ import annotations
22

3+
import tarfile
4+
import zipfile
5+
36
from pathlib import Path
47
from typing import TYPE_CHECKING
58
from typing import Any
9+
from typing import Literal
610

711
import requests
812

9-
from poetry.core.masonry.metadata import Metadata
13+
from packaging.metadata import RawMetadata
14+
from packaging.metadata import parse_email
1015
from poetry.core.masonry.utils.helpers import distribution_name
1116
from requests_toolbelt import user_agent
1217
from requests_toolbelt.multipart import MultipartEncoder
@@ -101,10 +106,9 @@ def upload(
101106
with session:
102107
self._upload(session, url, dry_run, skip_existing)
103108

104-
def post_data(self, file: Path) -> dict[str, Any]:
105-
meta = Metadata.from_package(self._package)
106-
107-
file_type = self._get_type(file)
109+
@classmethod
110+
def post_data(cls, file: Path) -> dict[str, Any]:
111+
file_type = cls._get_type(file)
108112

109113
hash_manager = HashManager()
110114
hash_manager.hash(file)
@@ -119,51 +123,38 @@ def post_data(self, file: Path) -> dict[str, Any]:
119123
wheel_info = wheel_file_re.match(file.name)
120124
if wheel_info is not None:
121125
py_version = wheel_info.group("pyver")
126+
else:
127+
py_version = "source"
122128

123-
data = {
124-
# identify release
125-
"name": meta.name,
126-
"version": meta.version,
127-
# file content
128-
"filetype": file_type,
129-
"pyversion": py_version,
130-
# additional meta-data
131-
"metadata_version": meta.metadata_version,
132-
"summary": meta.summary,
133-
"home_page": meta.home_page,
134-
"author": meta.author,
135-
"author_email": meta.author_email,
136-
"maintainer": meta.maintainer,
137-
"maintainer_email": meta.maintainer_email,
138-
"license": meta.license,
139-
"description": meta.description,
140-
"keywords": meta.keywords,
141-
"platform": meta.platforms,
142-
"classifiers": meta.classifiers,
143-
"download_url": meta.download_url,
144-
"supported_platform": meta.supported_platforms,
145-
"comment": None,
129+
data: dict[str, Any] = {
130+
# Upload API (https://docs.pypi.org/api/upload/)
131+
# ":action", "protocol_version" and "content are added later
146132
"md5_digest": md5_digest,
147133
"sha256_digest": sha2_digest,
148134
"blake2_256_digest": blake2_256_digest,
149-
# PEP 314
150-
"provides": meta.provides,
151-
"requires": meta.requires,
152-
"obsoletes": meta.obsoletes,
153-
# Metadata 1.2
154-
"project_urls": meta.project_urls,
155-
"provides_dist": meta.provides_dist,
156-
"obsoletes_dist": meta.obsoletes_dist,
157-
"requires_dist": meta.requires_dist,
158-
"requires_external": meta.requires_external,
159-
"requires_python": meta.requires_python,
135+
"filetype": file_type,
136+
"pyversion": py_version,
160137
}
161138

162-
# Metadata 2.1
163-
if meta.description_content_type:
164-
data["description_content_type"] = meta.description_content_type
139+
for key, value in cls._get_metadata(file).items():
140+
# strip trailing 's' to match API field names
141+
# see https://docs.pypi.org/api/upload/
142+
if key in {"platforms", "supported_platforms", "license_files"}:
143+
key = key[:-1]
165144

166-
# TODO: Provides extra
145+
# revert some special cases from packaging.metadata.parse_email()
146+
147+
# "keywords" is not "multiple use" but a comma-separated string
148+
if key == "keywords":
149+
assert isinstance(value, list)
150+
value = ", ".join(value)
151+
152+
# "project_urls" is not a dict
153+
if key == "project_urls":
154+
assert isinstance(value, dict)
155+
value = [f"{k}, {v}" for k, v in value.items()]
156+
157+
data[key] = value
167158

168159
return data
169160

@@ -191,13 +182,7 @@ def _upload_file(
191182
raise UploadError(f"Archive ({file}) does not exist")
192183

193184
data = self.post_data(file)
194-
data.update(
195-
{
196-
# action
197-
":action": "file_upload",
198-
"protocol_version": "1",
199-
}
200-
)
185+
data.update({":action": "file_upload", "protocol_version": "1"})
201186

202187
data_to_send: list[tuple[str, Any]] = self._prepare_data(data)
203188

@@ -308,15 +293,45 @@ def _prepare_data(self, data: dict[str, Any]) -> list[tuple[str, str]]:
308293

309294
return data_to_send
310295

311-
def _get_type(self, file: Path) -> str:
296+
@staticmethod
297+
def _get_type(file: Path) -> Literal["bdist_wheel", "sdist"]:
312298
exts = file.suffixes
313-
if exts[-1] == ".whl":
299+
if exts and exts[-1] == ".whl":
314300
return "bdist_wheel"
315301
elif len(exts) >= 2 and "".join(exts[-2:]) == ".tar.gz":
316302
return "sdist"
317303

318304
raise ValueError("Unknown distribution format " + "".join(exts))
319305

306+
@staticmethod
307+
def _get_metadata(file: Path) -> RawMetadata:
308+
if file.suffix == ".whl":
309+
with zipfile.ZipFile(file) as z:
310+
for name in z.namelist():
311+
parts = Path(name).parts
312+
if (
313+
len(parts) == 2
314+
and parts[1] == "METADATA"
315+
and parts[0].endswith(".dist-info")
316+
):
317+
with z.open(name) as mf:
318+
return parse_email(mf.read().decode("utf-8"))[0]
319+
raise FileNotFoundError("METADATA not found in wheel")
320+
321+
elif file.suffixes[-2:] == [".tar", ".gz"]:
322+
with tarfile.open(file, "r:gz") as tar:
323+
for member in tar.getmembers():
324+
parts = Path(member.name).parts
325+
if (
326+
len(parts) == 2
327+
and parts[1] == "PKG-INFO"
328+
and (pf := tar.extractfile(member))
329+
):
330+
return parse_email(pf.read().decode("utf-8"))[0]
331+
raise FileNotFoundError("PKG-INFO not found in sdist")
332+
333+
raise ValueError(f"Unsupported file type: {file}")
334+
320335
def _is_file_exists_error(self, response: requests.Response) -> bool:
321336
# based on https://github.com/pypa/twine/blob/a6dd69c79f7b5abfb79022092a5d3776a499e31b/twine/commands/upload.py#L32
322337
status = response.status_code

tests/publishing/test_uploader.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,78 @@ def test_uploader_properly_handles_file_not_existing(
155155
uploader.upload("https://foo.com")
156156

157157
assert f"Archive ({uploader.files[0]}) does not exist" == str(e.value)
158+
159+
160+
def test_uploader_post_data_wheel(fixture_dir: FixtureDirGetter) -> None:
161+
file = (
162+
fixture_dir("simple_project")
163+
/ "dist"
164+
/ "simple_project-1.2.3-py2.py3-none-any.whl"
165+
)
166+
assert Uploader.post_data(file) == {
167+
"md5_digest": "fb4a5266406b9cf34ceaa88d1c8b7a01",
168+
"sha256_digest": "fc365a242d4de8b8661babc088f44b3df25e9e0017ef5dd7140dfe50f9323e16",
169+
"blake2_256_digest": "2e006d1fbfef0ed38fbded1ec1614dc4fd66f81061fe290528e2744dbc25ce31",
170+
"filetype": "bdist_wheel",
171+
"pyversion": "py2.py3",
172+
"metadata_version": "2.1",
173+
"name": "simple-project",
174+
"version": "1.2.3",
175+
"summary": "Some description.",
176+
"author": "Sébastien Eustace",
177+
"author_email": "sebastien@eustace.io",
178+
"license": "MIT",
179+
"classifiers": [
180+
"License :: OSI Approved :: MIT License",
181+
"Programming Language :: Python :: 2",
182+
"Programming Language :: Python :: 2.7",
183+
"Programming Language :: Python :: 3",
184+
"Programming Language :: Python :: 3.6",
185+
"Programming Language :: Python :: 3.7",
186+
"Topic :: Software Development :: Build Tools",
187+
"Topic :: Software Development :: Libraries :: Python Modules",
188+
],
189+
"requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*",
190+
"description": "My Package\n==========\n\n",
191+
"description_content_type": "text/x-rst",
192+
"keywords": "packaging, dependency, poetry",
193+
"home_page": "https://poetry.eustace.io",
194+
"project_urls": [
195+
"Documentation, https://poetry.eustace.io/docs",
196+
"Repository, https://github.com/sdispater/poetry",
197+
],
198+
}
199+
200+
201+
def test_uploader_post_data_sdist(fixture_dir: FixtureDirGetter) -> None:
202+
file = fixture_dir("simple_project") / "dist" / "simple_project-1.2.3.tar.gz"
203+
assert Uploader.post_data(file) == {
204+
"md5_digest": "e611cbb8f31258243d90f7681dfda68a",
205+
"sha256_digest": "c4a72becabca29ec2a64bf8c820bbe204d2268f53e102501ea5605bc1c1675d1",
206+
"blake2_256_digest": "d3df22f4944f6acd02105e7e2df61ef63c7b0f4337a12df549ebc2805a13c2be",
207+
"filetype": "sdist",
208+
"pyversion": "source",
209+
"metadata_version": "2.1",
210+
"name": "simple-project",
211+
"version": "1.2.3",
212+
"summary": "Some description.",
213+
"author": "Sébastien Eustace",
214+
"author_email": "sebastien@eustace.io",
215+
"classifiers": [
216+
"License :: OSI Approved :: MIT License",
217+
"Programming Language :: Python :: 2",
218+
"Programming Language :: Python :: 2.7",
219+
"Programming Language :: Python :: 3",
220+
"Programming Language :: Python :: 3.6",
221+
"Programming Language :: Python :: 3.7",
222+
"Topic :: Software Development :: Build Tools",
223+
"Topic :: Software Development :: Libraries :: Python Modules",
224+
],
225+
"requires_python": ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*",
226+
"keywords": "packaging, dependency, poetry",
227+
"home_page": "https://poetry.eustace.io",
228+
"project_urls": [
229+
"Documentation, https://poetry.eustace.io/docs",
230+
"Repository, https://github.com/sdispater/poetry",
231+
],
232+
}

0 commit comments

Comments
 (0)