Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement PEP 639, Metadata 2.4 #16949

Merged
merged 23 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2620974
run from my fork of packaging temporarily
ewdurbin Oct 22, 2024
d68ac53
add License-File and License-Expression fields to Release
ewdurbin Sep 6, 2024
40b2309
store license-expression and license-files when included
ewdurbin Oct 22, 2024
c890d8d
enforce that license-files exist in distributions
ewdurbin Oct 22, 2024
3259eb8
enforce License/License-Expression mutual exclusion
ewdurbin Oct 22, 2024
94f0d4b
clarify Release.license_files contents in comment
ewdurbin Oct 23, 2024
15daa6e
refactor license-file check for tar.gz sdists
ewdurbin Oct 23, 2024
e555ab1
harmonize with the definition of "root license directory"/"license di…
ewdurbin Oct 23, 2024
b9a10ea
fix comment
ewdurbin Oct 23, 2024
26db8df
Merge branch 'main' into pep_639
ewdurbin Oct 24, 2024
0457f44
sync License-File(s) and License-Expression metadata to Big Query
ewdurbin Oct 24, 2024
326bb61
add license_files and license_expression to JSON API
ewdurbin Oct 24, 2024
101ce02
display License-Expression in project details if provided
ewdurbin Oct 24, 2024
a00fdef
move to first commit of pypa/packaging including spdx bits!
ewdurbin Oct 24, 2024
12379e5
Merge branch 'main' into pep_639
ewdurbin Oct 25, 2024
0709950
update to latest pypa/packaging commit
ewdurbin Nov 7, 2024
19f15cf
Merge branch 'main' into pep_639
ewdurbin Nov 7, 2024
2fadec1
Merge branch 'main' into pep_639
ewdurbin Nov 7, 2024
12e16a5
Merge branch 'main' into pep_639
ewdurbin Nov 8, 2024
a968dd5
Merge branch 'main' into pep_639
ewdurbin Nov 13, 2024
a8dd469
zip file license-files check: add note to remove when 625 is implemented
ewdurbin Nov 13, 2024
74b7b8c
add 2 minute lock and statement timeouts to migrations
ewdurbin Nov 13, 2024
8c2859a
remove redundant metadata version gate for license-files check
ewdurbin Nov 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion requirements/main.in
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ msgpack
natsort
opensearch-py
orjson
packaging>=23.2
packaging>=24.2
packaging_legacy
paginate>=0.5.2
paginate_sqlalchemy
Expand Down
278 changes: 277 additions & 1 deletion tests/unit/forklift/test_legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,28 @@ def _get_tar_testdata(compression_type=""):
temp_f = io.BytesIO()
with tarfile.open(fileobj=temp_f, mode=f"w:{compression_type}") as tar:
tar.add("/dev/null", arcname="fake_package/PKG-INFO")
tar.add("/dev/null", arcname="LICENSE.MIT")
tar.add("/dev/null", arcname="LICENSE.APACHE")
return temp_f.getvalue()


def _get_zip_testdata():
temp_f = io.BytesIO()
with zipfile.ZipFile(file=temp_f, mode="w") as zfp:
zfp.writestr("fake_package/PKG-INFO", "Fake PKG-INFO")
zfp.writestr("LICENSE.MIT", "Fake License")
zfp.writestr("LICENSE.APACHE", "Fake License")
return temp_f.getvalue()


def _get_whl_testdata(name="fake_package", version="1.0"):
temp_f = io.BytesIO()
with zipfile.ZipFile(file=temp_f, mode="w") as zfp:
zfp.writestr(f"{name}-{version}.dist-info/METADATA", "Fake metadata")
zfp.writestr(f"{name}-{version}.dist-info/licenses/LICENSE.MIT", "Fake License")
zfp.writestr(
f"{name}-{version}.dist-info/licenses/LICENSE.APACHE", "Fake License"
)
return temp_f.getvalue()


Expand All @@ -98,6 +113,12 @@ def _storage_hash(data):
_TAR_BZ2_PKG_STORAGE_HASH = _storage_hash(_TAR_BZ2_PKG_TESTDATA)


_ZIP_PKG_TESTDATA = _get_zip_testdata()
_ZIP_PKG_MD5 = hashlib.md5(_ZIP_PKG_TESTDATA).hexdigest()
_ZIP_PKG_SHA256 = hashlib.sha256(_ZIP_PKG_TESTDATA).hexdigest()
_ZIP_PKG_STORAGE_HASH = _storage_hash(_ZIP_PKG_TESTDATA)


class TestExcWithMessage:
def test_exc_with_message(self):
exc = legacy._exc_with_message(HTTPBadRequest, "My Test Message.")
Expand Down Expand Up @@ -2877,7 +2898,7 @@ def test_upload_succeeds_pep625_normalized_filename(
RoleFactory.create(user=user, project=project)

filename = f"{filename_prefix}-{version}.tar.gz"
filebody = _get_whl_testdata(name=project_name, version=version)
filebody = _TAR_GZ_PKG_TESTDATA

@pretend.call_recorder
def storage_service_store(path, file_path, *, meta):
Expand Down Expand Up @@ -4708,6 +4729,261 @@ def test_upload_with_token_api_warns_if_trusted_publisher_configured(
if not warning_already_sent:
assert not warning_exists

@pytest.mark.parametrize(
("version", "expected_version", "filetype", "mimetype"),
[
("1.0", "1.0", "sdist", "application/tar"),
("v1.0", "1.0", "sdist", "application/tar"),
("1.0", "1.0", "sdist", "application/zip"),
("v1.0", "1.0", "sdist", "application/zip"),
("1.0", "1.0", "bdist_wheel", "application/zip"),
("v1.0", "1.0", "bdist_wheel", "application/zip"),
],
)
def test_upload_succeeds_creates_release_metadata_2_4(
self,
pyramid_config,
db_request,
metrics,
monkeypatch,
version,
expected_version,
filetype,
mimetype,
):
user = UserFactory.create()
EmailFactory.create(user=user)
project = ProjectFactory.create()
RoleFactory.create(user=user, project=project)

if filetype == "sdist":
if mimetype == "application/tar":
filename = "{}-{}.tar.gz".format(project.name, "1.0")
digest = _TAR_GZ_PKG_MD5
data = _TAR_GZ_PKG_TESTDATA
elif mimetype == "application/zip":
filename = "{}-{}.zip".format(project.name, "1.0")
digest = _ZIP_PKG_MD5
data = _ZIP_PKG_TESTDATA
elif filetype == "bdist_wheel":
filename = "{}-{}-py3-none-any.whl".format(project.name, "1.0")
data = _get_whl_testdata(name=project.name, version="1.0")
digest = hashlib.md5(data).hexdigest()
monkeypatch.setattr(legacy, "_is_valid_dist_file", lambda *a, **kw: True)

pyramid_config.testing_securitypolicy(identity=user)
db_request.user = user
db_request.user_agent = "warehouse-tests/6.6.6"
db_request.POST = MultiDict(
{
"metadata_version": "2.4",
"name": project.name,
"version": version,
"summary": "This is my summary!",
"filetype": filetype,
"md5_digest": digest,
"content": pretend.stub(
filename=filename,
file=io.BytesIO(data),
type=mimetype,
),
}
)
db_request.POST.extend(
[
("license_expression", "MIT OR Apache-2.0"),
("license_files", "LICENSE.APACHE"),
("license_files", "LICENSE.MIT"),
]
)
if filetype == "bdist_wheel":
db_request.POST.extend([("pyversion", "py3")])

storage_service = pretend.stub(store=lambda path, filepath, meta: None)
db_request.find_service = lambda svc, name=None, context=None: {
IFileStorage: storage_service,
IMetricsService: metrics,
}.get(svc)

resp = legacy.file_upload(db_request)

assert resp.status_code == 200

# Ensure that a Release object has been created.
release = (
db_request.db.query(Release)
.filter(
(Release.project == project) & (Release.version == expected_version)
)
.one()
)
assert release.summary == "This is my summary!"
assert release.version == expected_version
assert release.canonical_version == "1"
assert release.uploaded_via == "warehouse-tests/6.6.6"
assert release.license_expression == "MIT OR Apache-2.0"
assert set(release.license_files) == {
"LICENSE.APACHE",
"LICENSE.MIT",
}

# Ensure that a File object has been created.
db_request.db.query(File).filter(
(File.release == release) & (File.filename == filename)
).one()

# Ensure that a Filename object has been created.
db_request.db.query(Filename).filter(Filename.filename == filename).one()

@pytest.mark.parametrize(
("version", "expected_version", "filetype", "mimetype"),
[
("1.0", "1.0", "sdist", "application/tar"),
("v1.0", "1.0", "sdist", "application/tar"),
("1.0", "1.0", "sdist", "application/zip"),
("v1.0", "1.0", "sdist", "application/zip"),
("1.0", "1.0", "bdist_wheel", "application/zip"),
("v1.0", "1.0", "bdist_wheel", "application/zip"),
],
)
def test_upload_fails_missing_license_file_metadata_2_4(
self,
pyramid_config,
db_request,
metrics,
monkeypatch,
version,
expected_version,
filetype,
mimetype,
):
user = UserFactory.create()
EmailFactory.create(user=user)
project = ProjectFactory.create()
RoleFactory.create(user=user, project=project)

if filetype == "sdist":
if mimetype == "application/tar":
filename = "{}-{}.tar.gz".format(project.name, "1.0")
digest = _TAR_GZ_PKG_MD5
data = _TAR_GZ_PKG_TESTDATA
elif mimetype == "application/zip":
filename = "{}-{}.zip".format(project.name, "1.0")
digest = _ZIP_PKG_MD5
data = _ZIP_PKG_TESTDATA
license_filename = "LICENSE"
elif filetype == "bdist_wheel":
filename = "{}-{}-py3-none-any.whl".format(project.name, "1.0")
data = _get_whl_testdata(name=project.name, version="1.0")
digest = hashlib.md5(data).hexdigest()
monkeypatch.setattr(legacy, "_is_valid_dist_file", lambda *a, **kw: True)
license_filename = f"{project.name}-1.0.dist-info/licenses/LICENSE"

pyramid_config.testing_securitypolicy(identity=user)
db_request.user = user
db_request.user_agent = "warehouse-tests/6.6.6"
db_request.POST = MultiDict(
{
"metadata_version": "2.4",
"name": project.name,
"version": version,
"summary": "This is my summary!",
"filetype": filetype,
"md5_digest": digest,
"content": pretend.stub(
filename=filename,
file=io.BytesIO(data),
type=mimetype,
),
}
)
db_request.POST.extend(
[
("license_expression", "MIT OR Apache-2.0"),
("license_files", "LICENSE"), # Does not exist in test data
("license_files", "LICENSE.MIT"),
]
)
if filetype == "bdist_wheel":
db_request.POST.extend([("pyversion", "py3")])

storage_service = pretend.stub(store=lambda path, filepath, meta: None)
db_request.find_service = lambda svc, name=None, context=None: {
IFileStorage: storage_service,
IMetricsService: metrics,
}.get(svc)

with pytest.raises(HTTPBadRequest) as excinfo:
legacy.file_upload(db_request)

resp = excinfo.value

assert resp.status_code == 400
assert resp.status == (
f"400 License-File {license_filename} does not exist "
f"in distribution file {filename}"
)

def test_upload_fails_when_license_and_license_expression_are_present(
ewdurbin marked this conversation as resolved.
Show resolved Hide resolved
self,
pyramid_config,
db_request,
metrics,
):
user = UserFactory.create()
EmailFactory.create(user=user)
project = ProjectFactory.create()
RoleFactory.create(user=user, project=project)

filename = "{}-{}-py3-none-any.whl".format(project.name, "1.0")
data = _get_whl_testdata(name=project.name, version="1.0")
digest = hashlib.md5(data).hexdigest()

pyramid_config.testing_securitypolicy(identity=user)
db_request.user = user
db_request.user_agent = "warehouse-tests/6.6.6"
db_request.POST = MultiDict(
{
"metadata_version": "2.4",
"name": project.name,
"version": "1.0",
"summary": "This is my summary!",
"filetype": "bdist_wheel",
"md5_digest": digest,
"content": pretend.stub(
filename=filename,
file=io.BytesIO(data),
type="application/zip",
),
}
)
db_request.POST.extend(
[
("license_expression", "MIT OR Apache-2.0"),
("license", "MIT LICENSE or Apache-2.0 License"),
]
)
db_request.POST.extend([("pyversion", "py3")])

storage_service = pretend.stub(store=lambda path, filepath, meta: None)
db_request.find_service = lambda svc, name=None, context=None: {
IFileStorage: storage_service,
IMetricsService: metrics,
}.get(svc)

with pytest.raises(HTTPBadRequest) as excinfo:
legacy.file_upload(db_request)

resp = excinfo.value

assert resp.status_code == 400
assert resp.status == (
"400 License is deprecated when License-Expression is present. "
"Only License-Expression should be present. "
"See https://packaging.python.org/specifications/core-metadata "
"for more information."
)


def test_submit(pyramid_request):
resp = legacy.submit(pyramid_request)
Expand Down
6 changes: 6 additions & 0 deletions tests/unit/legacy/api/test_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,8 @@ def test_renders(self, pyramid_config, db_request, db_session):
"home_page": None,
"keywords": None,
"license": None,
"license_expression": None,
"license_files": None,
"maintainer": None,
"maintainer_email": None,
"name": project.name,
Expand Down Expand Up @@ -572,6 +574,8 @@ def test_detail_renders(self, pyramid_config, db_request, db_session):
"home_page": None,
"keywords": None,
"license": None,
"license_expression": None,
"license_files": None,
"maintainer": None,
"maintainer_email": None,
"name": project.name,
Expand Down Expand Up @@ -664,6 +668,8 @@ def test_minimal_renders(self, pyramid_config, db_request):
"home_page": None,
"keywords": None,
"license": None,
"license_expression": None,
"license_files": None,
"maintainer": None,
"maintainer_email": None,
"name": project.name,
Expand Down
Loading