Skip to content

Commit b4a6319

Browse files
committed
Warn on upload about non-normalized wheel distribution name
1 parent a64d492 commit b4a6319

File tree

6 files changed

+205
-0
lines changed

6 files changed

+205
-0
lines changed

tests/unit/email/test_init.py

+87
Original file line numberDiff line numberDiff line change
@@ -6314,3 +6314,90 @@ def test_environment_ignored_in_trusted_publisher_emails(
63146314
},
63156315
),
63166316
]
6317+
6318+
def test_pep427_emails(
6319+
self,
6320+
pyramid_request,
6321+
pyramid_config,
6322+
monkeypatch,
6323+
):
6324+
stub_user = pretend.stub(
6325+
id="id",
6326+
username="username",
6327+
name="",
6328+
6329+
primary_email=pretend.stub(email="[email protected]", verified=True),
6330+
)
6331+
subject_renderer = pyramid_config.testing_add_renderer(
6332+
"email/pep427-name-email/subject.txt"
6333+
)
6334+
subject_renderer.string_response = "Email Subject"
6335+
body_renderer = pyramid_config.testing_add_renderer(
6336+
"email/pep427-name-email/body.txt"
6337+
)
6338+
body_renderer.string_response = "Email Body"
6339+
html_renderer = pyramid_config.testing_add_renderer(
6340+
"email/pep427-name-email/body.html"
6341+
)
6342+
html_renderer.string_response = "Email HTML Body"
6343+
6344+
send_email = pretend.stub(
6345+
delay=pretend.call_recorder(lambda *args, **kwargs: None)
6346+
)
6347+
pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email)
6348+
monkeypatch.setattr(email, "send_email", send_email)
6349+
6350+
pyramid_request.db = pretend.stub(
6351+
query=lambda a: pretend.stub(
6352+
filter=lambda *a: pretend.stub(
6353+
one=lambda: pretend.stub(user_id=stub_user.id)
6354+
)
6355+
),
6356+
)
6357+
pyramid_request.user = stub_user
6358+
pyramid_request.registry.settings = {"mail.sender": "[email protected]"}
6359+
6360+
project_name = "Test_Project"
6361+
filename = "Test_Project-1.0-py3-none-any.whl"
6362+
6363+
result = email.send_pep427_name_email(
6364+
pyramid_request,
6365+
stub_user,
6366+
project_name=project_name,
6367+
filename=filename,
6368+
normalized_name="test_project",
6369+
)
6370+
6371+
assert result == {
6372+
"project_name": project_name,
6373+
"normalized_name": "test_project",
6374+
"filename": filename,
6375+
}
6376+
subject_renderer.assert_(project_name=project_name)
6377+
body_renderer.assert_(project_name=project_name)
6378+
html_renderer.assert_(project_name=project_name)
6379+
assert pyramid_request.task.calls == [pretend.call(send_email)]
6380+
assert send_email.delay.calls == [
6381+
pretend.call(
6382+
f"{stub_user.username} <{stub_user.email}>",
6383+
{
6384+
"sender": None,
6385+
"subject": "Email Subject",
6386+
"body_text": "Email Body",
6387+
"body_html": (
6388+
"<html>\n<head></head>\n"
6389+
"<body><p>Email HTML Body</p></body>\n</html>\n"
6390+
),
6391+
},
6392+
{
6393+
"tag": "account:email:sent",
6394+
"user_id": stub_user.id,
6395+
"additional": {
6396+
"from_": "[email protected]",
6397+
"to": stub_user.email,
6398+
"subject": "Email Subject",
6399+
"redact_ip": False,
6400+
},
6401+
},
6402+
)
6403+
]

warehouse/email/__init__.py

+9
Original file line numberDiff line numberDiff line change
@@ -1104,6 +1104,15 @@ def send_environment_ignored_in_trusted_publisher_email(
11041104
}
11051105

11061106

1107+
@_email("pep427-name-email")
1108+
def send_pep427_name_email(request, users, project_name, filename, normalized_name):
1109+
return {
1110+
"project_name": project_name,
1111+
"filename": filename,
1112+
"normalized_name": normalized_name,
1113+
}
1114+
1115+
11071116
def includeme(config):
11081117
email_sending_class = config.maybe_dotted(config.registry.settings["mail.backend"])
11091118
config.register_service_factory(email_sending_class.create_service, IEmailSender)

warehouse/forklift/legacy.py

+19
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
from warehouse.constants import MAX_FILESIZE, MAX_PROJECT_SIZE, ONE_GIB, ONE_MIB
5353
from warehouse.email import (
5454
send_api_token_used_in_trusted_publisher_project_email,
55+
send_pep427_name_email,
5556
send_pep625_extension_email,
5657
send_pep625_name_email,
5758
send_pep625_version_email,
@@ -1372,6 +1373,24 @@ def file_upload(request):
13721373
f"{canonical_name.replace('-', '_')!r}.",
13731374
)
13741375

1376+
# The parse_wheel_filename function does not enforce lowercasing,
1377+
# and also returns a normalized name, so we must get the original
1378+
# distribution name from the filename manually
1379+
name_from_filename, _ = filename.split("-", 1)
1380+
1381+
# PEP 427 / PEP 503: Enforcement of project name normalization.
1382+
# Filenames that do not start with the fully normalized project name
1383+
# will not be permitted.
1384+
# https://packaging.python.org/en/latest/specifications/binary-distribution-format/#escaping-and-unicode
1385+
if name_from_filename != name_from_filename.lower():
1386+
send_pep427_name_email(
1387+
request,
1388+
set(project.users),
1389+
project_name=project.name,
1390+
filename=filename,
1391+
normalized_name=project.normalized_name.replace("-", "_"),
1392+
)
1393+
13751394
if meta.version != version:
13761395
request.metrics.increment(
13771396
"warehouse.upload.failed",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{#
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
-#}
14+
{% extends "email/_base/body.html" %}
15+
16+
{% set domain = request.registry.settings.get('warehouse.domain') %}
17+
18+
{% block content %}
19+
<p>
20+
This email is notifying you of an upcoming deprecation that we have
21+
determined may affect you as a result of your recent upload to
22+
'{{ project_name }}'.
23+
</p>
24+
<p>
25+
In the future, PyPI will require all newly uploaded binary distribution
26+
filenames to comply with the <a href="https://packaging.python.org/en/latest/specifications/binary-distribution-format/">binary distribution format</a>.
27+
Any binary distributions already uploaded will remain in place
28+
as-is and do not need to be updated.
29+
</p>
30+
<p>
31+
Specifically, your recent upload of '{{ filename }}' is incompatible with
32+
the distribution format specification because it does not contain the normalized project
33+
name '{{ normalized_name }}'.
34+
</p>
35+
<p>
36+
In most cases, this can be resolved by upgrading the version of your build
37+
tooling to a later version that fully supports the specification and
38+
produces compliant filenames.
39+
</p>
40+
<p>
41+
If you have questions, you can email
42+
<a href="mailto:[email protected]">[email protected]</a> to communicate with the PyPI
43+
[email protected] to communicate with the PyPI administrators.
44+
</p>
45+
{% endblock %}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{#
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
-#}
14+
{% extends "email/_base/body.txt" %}
15+
16+
{% block content %}
17+
18+
This email is notifying you of an upcoming deprecation that we have determined may affect you as a result of your recent upload to '{{ project_name }}'.
19+
20+
In the future, PyPI will require all newly uploaded binary distribution filenames to comply with the <a href="https://packaging.python.org/en/latest/specifications/binary-distribution-format/">binary distribution format</a>. Any binary distributions already uploaded will remain in place as-is and do not need to be updated.
21+
22+
Specifically, your recent upload of '{{ filename }}' is incompatible with the distribution format specification because it does not contain the normalized project name '{{ normalized_name }}'.
23+
24+
In most cases, this can be resolved by upgrading the version of your build tooling to a later version that fully supports the specification and produces compliant filenames.
25+
26+
If you have questions, you can email [email protected] to communicate with the PyPI administrators.
27+
28+
{% endblock %}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{#
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
#
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
-#}
14+
15+
{% extends "email/_base/subject.txt" %}
16+
17+
{% block subject %}Deprecation notice for recent binary distribution upload to '{{ project_name }}'{% endblock %}

0 commit comments

Comments
 (0)