Skip to content

Commit

Permalink
Warn on upload about non-normalized wheel distribution name
Browse files Browse the repository at this point in the history
  • Loading branch information
di committed Feb 25, 2025
1 parent a64d492 commit b4a6319
Show file tree
Hide file tree
Showing 6 changed files with 205 additions and 0 deletions.
87 changes: 87 additions & 0 deletions tests/unit/email/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -6314,3 +6314,90 @@ def test_environment_ignored_in_trusted_publisher_emails(
},
),
]

def test_pep427_emails(
self,
pyramid_request,
pyramid_config,
monkeypatch,
):
stub_user = pretend.stub(
id="id",
username="username",
name="",
email="[email protected]",
primary_email=pretend.stub(email="[email protected]", verified=True),
)
subject_renderer = pyramid_config.testing_add_renderer(
"email/pep427-name-email/subject.txt"
)
subject_renderer.string_response = "Email Subject"
body_renderer = pyramid_config.testing_add_renderer(
"email/pep427-name-email/body.txt"
)
body_renderer.string_response = "Email Body"
html_renderer = pyramid_config.testing_add_renderer(
"email/pep427-name-email/body.html"
)
html_renderer.string_response = "Email HTML Body"

send_email = pretend.stub(
delay=pretend.call_recorder(lambda *args, **kwargs: None)
)
pyramid_request.task = pretend.call_recorder(lambda *args, **kwargs: send_email)
monkeypatch.setattr(email, "send_email", send_email)

pyramid_request.db = pretend.stub(
query=lambda a: pretend.stub(
filter=lambda *a: pretend.stub(
one=lambda: pretend.stub(user_id=stub_user.id)
)
),
)
pyramid_request.user = stub_user
pyramid_request.registry.settings = {"mail.sender": "[email protected]"}

project_name = "Test_Project"
filename = "Test_Project-1.0-py3-none-any.whl"

result = email.send_pep427_name_email(
pyramid_request,
stub_user,
project_name=project_name,
filename=filename,
normalized_name="test_project",
)

assert result == {
"project_name": project_name,
"normalized_name": "test_project",
"filename": filename,
}
subject_renderer.assert_(project_name=project_name)
body_renderer.assert_(project_name=project_name)
html_renderer.assert_(project_name=project_name)
assert pyramid_request.task.calls == [pretend.call(send_email)]
assert send_email.delay.calls == [
pretend.call(
f"{stub_user.username} <{stub_user.email}>",
{
"sender": None,
"subject": "Email Subject",
"body_text": "Email Body",
"body_html": (
"<html>\n<head></head>\n"
"<body><p>Email HTML Body</p></body>\n</html>\n"
),
},
{
"tag": "account:email:sent",
"user_id": stub_user.id,
"additional": {
"from_": "[email protected]",
"to": stub_user.email,
"subject": "Email Subject",
"redact_ip": False,
},
},
)
]
9 changes: 9 additions & 0 deletions warehouse/email/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1104,6 +1104,15 @@ def send_environment_ignored_in_trusted_publisher_email(
}


@_email("pep427-name-email")
def send_pep427_name_email(request, users, project_name, filename, normalized_name):
return {
"project_name": project_name,
"filename": filename,
"normalized_name": normalized_name,
}


def includeme(config):
email_sending_class = config.maybe_dotted(config.registry.settings["mail.backend"])
config.register_service_factory(email_sending_class.create_service, IEmailSender)
Expand Down
19 changes: 19 additions & 0 deletions warehouse/forklift/legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
from warehouse.constants import MAX_FILESIZE, MAX_PROJECT_SIZE, ONE_GIB, ONE_MIB
from warehouse.email import (
send_api_token_used_in_trusted_publisher_project_email,
send_pep427_name_email,
send_pep625_extension_email,
send_pep625_name_email,
send_pep625_version_email,
Expand Down Expand Up @@ -1372,6 +1373,24 @@ def file_upload(request):
f"{canonical_name.replace('-', '_')!r}.",
)

# The parse_wheel_filename function does not enforce lowercasing,
# and also returns a normalized name, so we must get the original
# distribution name from the filename manually
name_from_filename, _ = filename.split("-", 1)

# PEP 427 / PEP 503: Enforcement of project name normalization.
# Filenames that do not start with the fully normalized project name
# will not be permitted.
# https://packaging.python.org/en/latest/specifications/binary-distribution-format/#escaping-and-unicode
if name_from_filename != name_from_filename.lower():
send_pep427_name_email(
request,
set(project.users),
project_name=project.name,
filename=filename,
normalized_name=project.normalized_name.replace("-", "_"),
)

if meta.version != version:
request.metrics.increment(
"warehouse.upload.failed",
Expand Down
45 changes: 45 additions & 0 deletions warehouse/templates/email/pep427-name-email/body.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
-#}
{% extends "email/_base/body.html" %}

{% set domain = request.registry.settings.get('warehouse.domain') %}

{% block content %}
<p>
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 }}'.
</p>
<p>
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.
</p>
<p>
Specifically, your recent upload of '{{ filename }}' is incompatible with
the distribution format specification because it does not contain the normalized project
name '{{ normalized_name }}'.
</p>
<p>
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.
</p>
<p>
If you have questions, you can email
<a href="mailto:[email protected]">[email protected]</a> to communicate with the PyPI
[email protected] to communicate with the PyPI administrators.
</p>
{% endblock %}
28 changes: 28 additions & 0 deletions warehouse/templates/email/pep427-name-email/body.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
-#}
{% extends "email/_base/body.txt" %}

{% block content %}

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 }}'.

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.

Specifically, your recent upload of '{{ filename }}' is incompatible with the distribution format specification because it does not contain the normalized project name '{{ normalized_name }}'.

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.

If you have questions, you can email [email protected] to communicate with the PyPI administrators.

{% endblock %}
17 changes: 17 additions & 0 deletions warehouse/templates/email/pep427-name-email/subject.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
-#}

{% extends "email/_base/subject.txt" %}

{% block subject %}Deprecation notice for recent binary distribution upload to '{{ project_name }}'{% endblock %}

0 comments on commit b4a6319

Please sign in to comment.