Skip to content

Commit 106dbf0

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 7c4e9e3 commit 106dbf0

2 files changed

Lines changed: 137 additions & 51 deletions

File tree

src/poetry/publishing/uploader.py

Lines changed: 62 additions & 51 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,36 @@ 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+
value = ", ".join(value)
150+
151+
# "project_urls" is not a dict
152+
if key == "project_urls":
153+
value = [f"{k}, {v}" for k, v in value.items()]
154+
155+
data[key] = value
167156

168157
return data
169158

@@ -191,13 +180,7 @@ def _upload_file(
191180
raise UploadError(f"Archive ({file}) does not exist")
192181

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

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

@@ -308,7 +291,8 @@ def _prepare_data(self, data: dict[str, Any]) -> list[tuple[str, str]]:
308291

309292
return data_to_send
310293

311-
def _get_type(self, file: Path) -> str:
294+
@staticmethod
295+
def _get_type(file: Path) -> Literal["bdist_wheel", "sdist"]:
312296
exts = file.suffixes
313297
if exts[-1] == ".whl":
314298
return "bdist_wheel"
@@ -317,6 +301,33 @@ def _get_type(self, file: Path) -> str:
317301

318302
raise ValueError("Unknown distribution format " + "".join(exts))
319303

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

tests/publishing/test_uploader.py

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

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

0 commit comments

Comments
 (0)