From d7e8d8c82b1efe47f18bf424c64c65ed752d0d51 Mon Sep 17 00:00:00 2001
From: Tzu-ping Chung
Date: Wed, 22 Sep 2021 19:38:23 +0800
Subject: [PATCH 1/6] Use PEP 503 rules to validate upload filename
---
tests/unit/forklift/test_legacy.py | 22 ++++++++++++++++------
warehouse/forklift/legacy.py | 23 +++++++++++++++++++----
2 files changed, 35 insertions(+), 10 deletions(-)
diff --git a/tests/unit/forklift/test_legacy.py b/tests/unit/forklift/test_legacy.py
index b2f9ab3479bf..c0d79f9f333b 100644
--- a/tests/unit/forklift/test_legacy.py
+++ b/tests/unit/forklift/test_legacy.py
@@ -20,7 +20,6 @@
from cgi import FieldStorage
from unittest import mock
-import pkg_resources
import pretend
import pytest
import requests
@@ -2354,7 +2353,7 @@ def test_upload_fails_with_diff_filename_same_blake2(
"filetype": "sdist",
"md5_digest": hashlib.md5(file_content.getvalue()).hexdigest(),
"content": pretend.stub(
- filename="{}-fake.tar.gz".format(project.name),
+ filename="{}-0.1.tar.gz".format(project.name),
file=file_content,
type="application/tar",
),
@@ -2388,7 +2387,18 @@ def test_upload_fails_with_diff_filename_same_blake2(
"400 File already exists. See /the/help/url/ for more information."
)
- def test_upload_fails_with_wrong_filename(self, pyramid_config, db_request):
+ @pytest.mark.parametrize(
+ "filename",
+ [
+ "nope-{version}.tar.gz",
+ "nope-{version}-py3-none-any.whl",
+ "nope-notaversion.tar.gz",
+ "nope-notaversion-py3-none-any.whl",
+ ],
+ )
+ def test_upload_fails_with_wrong_filename(
+ self, pyramid_config, db_request, filename
+ ):
pyramid_config.testing_securitypolicy(userid=1)
user = UserFactory.create()
@@ -2398,7 +2408,7 @@ def test_upload_fails_with_wrong_filename(self, pyramid_config, db_request):
release = ReleaseFactory.create(project=project, version="1.0")
RoleFactory.create(user=user, project=project)
- filename = "nope-{}.tar.gz".format(release.version)
+ filename = filename.format(version=release.version)
db_request.POST = MultiDict(
{
@@ -2422,8 +2432,8 @@ def test_upload_fails_with_wrong_filename(self, pyramid_config, db_request):
assert resp.status_code == 400
assert resp.status == (
- "400 Start filename for {!r} with {!r}.".format(
- project.name, pkg_resources.safe_name(project.name).lower()
+ "400 Filename {!r} must match project {!r}.".format(
+ filename, project.normalized_name
)
)
diff --git a/warehouse/forklift/legacy.py b/warehouse/forklift/legacy.py
index 5542d09090c2..35e825668792 100644
--- a/warehouse/forklift/legacy.py
+++ b/warehouse/forklift/legacy.py
@@ -26,7 +26,6 @@
import packaging.specifiers
import packaging.utils
import packaging.version
-import pkg_resources
import requests
import stdlib_list
import wtforms
@@ -628,6 +627,22 @@ def full_validate(self):
)
+def _is_valid_filename(filename, specified_normalized_name):
+ if filename.endswith(".whl"):
+ parse_func = packaging.utils.parse_wheel_filename
+ else:
+ parse_func = packaging.utils.parse_sdist_filename
+ try:
+ parsed_parts = parse_func(filename)
+ except (
+ packaging.utils.InvalidSdistFilename,
+ packaging.utils.InvalidWheelFilename,
+ packaging.version.InvalidVersion,
+ ):
+ return False
+ return parsed_parts[0] == specified_normalized_name
+
+
_safe_zipnames = re.compile(r"(purelib|platlib|headers|scripts|data).+", re.I)
# .tar uncompressed, .tar.gz .tgz, .tar.bz2 .tbz2
_tar_filenames_re = re.compile(r"\.(?:tar$|t(?:ar\.)?(?Pgz|bz2)$)")
@@ -1194,11 +1209,11 @@ def file_upload(request):
# Make sure that our filename matches the project that it is being uploaded
# to.
- prefix = pkg_resources.safe_name(project.name).lower()
- if not pkg_resources.safe_name(filename).lower().startswith(prefix):
+ normalized_name = project.normalized_name
+ if not _is_valid_filename(filename, normalized_name):
raise _exc_with_message(
HTTPBadRequest,
- "Start filename for {!r} with {!r}.".format(project.name, prefix),
+ "Filename {!r} must match project {!r}.".format(filename, normalized_name),
)
# Check the content type of what is being uploaded
From 2fcf144cd5f19f396492c38cf7e4c1949953ef6f Mon Sep 17 00:00:00 2001
From: Dustin Ingram
Date: Mon, 13 Dec 2021 16:39:03 -0500
Subject: [PATCH 2/6] Some refactoring and improving error messages
---
tests/unit/forklift/test_legacy.py | 52 ++++++++++++++++++++++++++----
warehouse/forklift/legacy.py | 51 +++++++++++++++++------------
2 files changed, 76 insertions(+), 27 deletions(-)
diff --git a/tests/unit/forklift/test_legacy.py b/tests/unit/forklift/test_legacy.py
index c0d79f9f333b..5643583b11a4 100644
--- a/tests/unit/forklift/test_legacy.py
+++ b/tests/unit/forklift/test_legacy.py
@@ -2392,11 +2392,55 @@ def test_upload_fails_with_diff_filename_same_blake2(
[
"nope-{version}.tar.gz",
"nope-{version}-py3-none-any.whl",
+ ],
+ )
+ def test_upload_fails_with_wrong_prefix(self, pyramid_config, db_request, filename):
+ pyramid_config.testing_securitypolicy(userid=1)
+
+ user = UserFactory.create()
+ db_request.user = user
+ EmailFactory.create(user=user)
+ project = ProjectFactory.create()
+ release = ReleaseFactory.create(project=project, version="1.0")
+ RoleFactory.create(user=user, project=project)
+
+ filename = filename.format(version=release.version)
+
+ db_request.POST = MultiDict(
+ {
+ "metadata_version": "1.2",
+ "name": project.name,
+ "version": release.version,
+ "filetype": "sdist",
+ "md5_digest": "nope!",
+ "content": pretend.stub(
+ filename=filename,
+ file=io.BytesIO(b"a" * (legacy.MAX_FILESIZE + 1)),
+ type="application/tar",
+ ),
+ }
+ )
+
+ with pytest.raises(HTTPBadRequest) as excinfo:
+ legacy.file_upload(db_request)
+
+ resp = excinfo.value
+
+ assert resp.status_code == 400
+ assert resp.status == (
+ "400 Invalid filename: Start filename for project {!r} with {!r}.".format(
+ project.name, project.normalized_name.replace("-", "_")
+ )
+ )
+
+ @pytest.mark.parametrize(
+ "filename",
+ [
"nope-notaversion.tar.gz",
"nope-notaversion-py3-none-any.whl",
],
)
- def test_upload_fails_with_wrong_filename(
+ def test_upload_fails_with_invalid_version(
self, pyramid_config, db_request, filename
):
pyramid_config.testing_securitypolicy(userid=1)
@@ -2431,11 +2475,7 @@ def test_upload_fails_with_wrong_filename(
resp = excinfo.value
assert resp.status_code == 400
- assert resp.status == (
- "400 Filename {!r} must match project {!r}.".format(
- filename, project.normalized_name
- )
- )
+ assert resp.status == "400 Invalid filename: Invalid version: 'notaversion'."
def test_upload_fails_with_invalid_extension(self, pyramid_config, db_request):
pyramid_config.testing_securitypolicy(userid=1)
diff --git a/warehouse/forklift/legacy.py b/warehouse/forklift/legacy.py
index 35e825668792..d63793e58965 100644
--- a/warehouse/forklift/legacy.py
+++ b/warehouse/forklift/legacy.py
@@ -627,22 +627,6 @@ def full_validate(self):
)
-def _is_valid_filename(filename, specified_normalized_name):
- if filename.endswith(".whl"):
- parse_func = packaging.utils.parse_wheel_filename
- else:
- parse_func = packaging.utils.parse_sdist_filename
- try:
- parsed_parts = parse_func(filename)
- except (
- packaging.utils.InvalidSdistFilename,
- packaging.utils.InvalidWheelFilename,
- packaging.version.InvalidVersion,
- ):
- return False
- return parsed_parts[0] == specified_normalized_name
-
-
_safe_zipnames = re.compile(r"(purelib|platlib|headers|scripts|data).+", re.I)
# .tar uncompressed, .tar.gz .tgz, .tar.bz2 .tbz2
_tar_filenames_re = re.compile(r"\.(?:tar$|t(?:ar\.)?(?Pgz|bz2)$)")
@@ -750,6 +734,15 @@ def _is_valid_dist_file(filename, filetype):
return True
+def _parse_filename(filename):
+ if filename.endswith(".whl"):
+ parse_func = packaging.utils.parse_wheel_filename
+ else:
+ parse_func = packaging.utils.parse_sdist_filename
+
+ return parse_func(filename)
+
+
def _is_duplicate_file(db_session, filename, hashes):
"""
Check to see if file already exists, and if it's content matches.
@@ -1207,13 +1200,29 @@ def file_upload(request):
"for more information.",
)
- # Make sure that our filename matches the project that it is being uploaded
- # to.
- normalized_name = project.normalized_name
- if not _is_valid_filename(filename, normalized_name):
+ try:
+ parsed_filename_parts = _parse_filename(filename)
+ except (
+ packaging.utils.InvalidSdistFilename,
+ packaging.utils.InvalidWheelFilename,
+ packaging.version.InvalidVersion,
+ ) as e:
+ raise _exc_with_message(HTTPBadRequest, f"Invalid filename: {e}.")
+
+ # Make sure that the normalized version of the project name in the filename
+ # matches the normalized project name that it is being uploaded to.
+ #
+ # NB: This allows periods the project name, which violates the wheel spec
+ # at [1], which requires that the filename start with the PEP 503
+ # normalization of the project name, followed by replacing - with _.
+ #
+ # [1] https://packaging.python.org/specifications/binary-distribution-format/
+ if not parsed_filename_parts[0] == project.normalized_name:
raise _exc_with_message(
HTTPBadRequest,
- "Filename {!r} must match project {!r}.".format(filename, normalized_name),
+ "Invalid filename: Start filename for project {!r} with {!r}.".format(
+ project.name, project.normalized_name.replace("-", "_")
+ ),
)
# Check the content type of what is being uploaded
From 6a79492cf4f9add30786fe1a71cc9a8b7a1204a5 Mon Sep 17 00:00:00 2001
From: Dustin Ingram
Date: Mon, 13 Dec 2021 19:56:52 -0500
Subject: [PATCH 3/6] Add test for existing wheel filename behavior
---
tests/unit/forklift/test_legacy.py | 53 ++++++++++++++++++++++++++++++
1 file changed, 53 insertions(+)
diff --git a/tests/unit/forklift/test_legacy.py b/tests/unit/forklift/test_legacy.py
index 5643583b11a4..b560cf0eaf75 100644
--- a/tests/unit/forklift/test_legacy.py
+++ b/tests/unit/forklift/test_legacy.py
@@ -2433,6 +2433,59 @@ def test_upload_fails_with_wrong_prefix(self, pyramid_config, db_request, filena
)
)
+ def test_wheel_upload_passes_with_period_in_prefix(
+ self, pyramid_config, db_request, metrics, monkeypatch
+ ):
+ """
+ This tests existing behavior which allows periods in the project name,
+ which violates the wheel spec at [1], which requires that the filename
+ start with the PEP 503 normalization of the project name, followed by
+ replacing - with _.
+
+ [1] https://packaging.python.org/specifications/binary-distribution-format/
+ """
+ pyramid_config.testing_securitypolicy(userid=1)
+
+ user = UserFactory.create()
+ db_request.user = user
+ EmailFactory.create(user=user)
+ project = ProjectFactory.create(name="some.project")
+ release = ReleaseFactory.create(project=project, version="1.0")
+ RoleFactory.create(user=user, project=project)
+
+ filename = "{project_name}-{version}-py3-none-any.whl".format(
+ version=release.version, project_name=project.name
+ )
+ monkeypatch.setattr(legacy, "_is_valid_dist_file", lambda *a, **kw: True)
+
+ db_request.find_service = pretend.call_recorder(
+ lambda svc, name=None, context=None: {
+ IFileStorage: pretend.stub(store=lambda *a, **kw: None),
+ IMetricsService: metrics,
+ }.get(svc)
+ )
+
+ db_request.user_agent = "warehouse-tests/6.6.6"
+ db_request.POST = MultiDict(
+ {
+ "metadata_version": "1.2",
+ "name": project.name,
+ "version": release.version,
+ "filetype": "bdist_wheel",
+ "pyversion": "py3",
+ "md5_digest": _TAR_GZ_PKG_MD5,
+ "content": pretend.stub(
+ filename=filename,
+ file=io.BytesIO(_TAR_GZ_PKG_TESTDATA),
+ type="application/tar",
+ ),
+ }
+ )
+
+ resp = legacy.file_upload(db_request)
+
+ assert resp.status_code == 200
+
@pytest.mark.parametrize(
"filename",
[
From ea0289cda46cb4e038277c0156c99aa2c8f49489 Mon Sep 17 00:00:00 2001
From: Dustin Ingram
Date: Mon, 13 Dec 2021 20:44:50 -0500
Subject: [PATCH 4/6] Canonicalize filename when comparing for duplicates
---
tests/unit/forklift/test_legacy.py | 39 ++++++++++++++++++++++++++++++
warehouse/forklift/legacy.py | 3 ++-
2 files changed, 41 insertions(+), 1 deletion(-)
diff --git a/tests/unit/forklift/test_legacy.py b/tests/unit/forklift/test_legacy.py
index b560cf0eaf75..ca86f1e39b2b 100644
--- a/tests/unit/forklift/test_legacy.py
+++ b/tests/unit/forklift/test_legacy.py
@@ -666,6 +666,45 @@ def test_is_duplicate_true(self, pyramid_config, db_request):
assert legacy._is_duplicate_file(db_request.db, filename, hashes)
+ @pytest.mark.parametrize("file_name_prefix", ["foo_bar", "foo.bar"])
+ @pytest.mark.parametrize("project_name", ["foo_bar", "foo.bar"])
+ def test_is_duplicate_true_non_normalized_filename(
+ self, pyramid_config, db_request, file_name_prefix, project_name
+ ):
+ pyramid_config.testing_securitypolicy(userid=1)
+
+ user = UserFactory.create()
+ EmailFactory.create(user=user)
+ project = ProjectFactory.create(name=project_name)
+ release = ReleaseFactory.create(project=project, version="1.0")
+ RoleFactory.create(user=user, project=project)
+
+ filename = "{}-{}.tar.gz".format(project_name, release.version)
+ file_content = io.BytesIO(_TAR_GZ_PKG_TESTDATA)
+ file_value = file_content.getvalue()
+
+ hashes = {
+ "sha256": hashlib.sha256(file_value).hexdigest(),
+ "md5": hashlib.md5(file_value).hexdigest(),
+ "blake2_256": hashlib.blake2b(file_value, digest_size=256 // 8).hexdigest(),
+ }
+ db_request.db.add(
+ File(
+ release=release,
+ filename=filename,
+ md5_digest=hashes["md5"],
+ sha256_digest=hashes["sha256"],
+ blake2_256_digest=hashes["blake2_256"],
+ path="source/{name[0]}/{name}/{filename}".format(
+ name=project.name, filename=filename
+ ),
+ )
+ )
+
+ duplicate_filename = "{}-{}.tar.gz".format(file_name_prefix, release.version)
+
+ assert legacy._is_duplicate_file(db_request.db, duplicate_filename, hashes)
+
def test_is_duplicate_none(self, pyramid_config, db_request):
pyramid_config.testing_securitypolicy(userid=1)
diff --git a/warehouse/forklift/legacy.py b/warehouse/forklift/legacy.py
index d63793e58965..1f735ec62417 100644
--- a/warehouse/forklift/legacy.py
+++ b/warehouse/forklift/legacy.py
@@ -766,7 +766,8 @@ def _is_duplicate_file(db_session, filename, hashes):
if file_ is not None:
return (
- file_.filename == filename
+ # This has the effect of canonicalizing the project name and version
+ _parse_filename(file_.filename) == _parse_filename(filename)
and file_.sha256_digest == hashes["sha256"]
and file_.md5_digest == hashes["md5"]
and file_.blake2_256_digest == hashes["blake2_256"]
From 99d19174119dcd7ec7763f9618284c53998b1408 Mon Sep 17 00:00:00 2001
From: Tzu-ping Chung
Date: Tue, 19 Apr 2022 13:03:16 +0800
Subject: [PATCH 5/6] Bump packaging requirement lower bound
This is needed to guarantee parse_[sdist|wheel]_filename functions.
---
requirements/main.in | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/main.in b/requirements/main.in
index ff62e5248969..9c40b32d6cbc 100644
--- a/requirements/main.in
+++ b/requirements/main.in
@@ -25,7 +25,7 @@ lxml
mistune
msgpack
natsort
-packaging>=15.2
+packaging>=20.9
paginate>=0.5.2
paginate_sqlalchemy
passlib>=1.6.4
From b14eff71e8c2bbf8949035c93437537008fc1a5a Mon Sep 17 00:00:00 2001
From: Tzu-ping Chung
Date: Tue, 19 Apr 2022 13:19:04 +0800
Subject: [PATCH 6/6] Add documentation for "Invalid filename" error
And link to it in the error message.
---
warehouse/forklift/legacy.py | 8 +-
warehouse/locale/messages.pot | 553 +++++++++++++++-------------
warehouse/templates/pages/help.html | 15 +
3 files changed, 317 insertions(+), 259 deletions(-)
diff --git a/warehouse/forklift/legacy.py b/warehouse/forklift/legacy.py
index a487be80c0e6..49235986f312 100644
--- a/warehouse/forklift/legacy.py
+++ b/warehouse/forklift/legacy.py
@@ -1256,11 +1256,13 @@ def file_upload(request):
#
# [1] https://packaging.python.org/specifications/binary-distribution-format/
if not parsed_filename_parts[0] == project.normalized_name:
+ escaped_normalized_project_name = project.normalized_name.replace("-", "_")
+ help_url = request.help_url(_anchor="invalid-file-name")
raise _exc_with_message(
HTTPBadRequest,
- "Invalid filename: Start filename for project {!r} with {!r}.".format(
- project.name, project.normalized_name.replace("-", "_")
- ),
+ f"Invalid filename: Start filename for project {project.name!r} "
+ f"with {escaped_normalized_project_name!r}. "
+ f"See {help_url} for more information",
)
# Check the content type of what is being uploaded
diff --git a/warehouse/locale/messages.pot b/warehouse/locale/messages.pot
index 45cbb56c6e03..f0f9ee8d930b 100644
--- a/warehouse/locale/messages.pot
+++ b/warehouse/locale/messages.pot
@@ -376,68 +376,68 @@ msgstr ""
#: warehouse/templates/packaging/detail.html:350
#: warehouse/templates/pages/classifiers.html:25
#: warehouse/templates/pages/help.html:20
-#: warehouse/templates/pages/help.html:208
-#: warehouse/templates/pages/help.html:215
-#: warehouse/templates/pages/help.html:229
-#: warehouse/templates/pages/help.html:245
-#: warehouse/templates/pages/help.html:249
-#: warehouse/templates/pages/help.html:306
-#: warehouse/templates/pages/help.html:333
-#: warehouse/templates/pages/help.html:338
-#: warehouse/templates/pages/help.html:343
+#: warehouse/templates/pages/help.html:210
+#: warehouse/templates/pages/help.html:217
+#: warehouse/templates/pages/help.html:231
+#: warehouse/templates/pages/help.html:247
+#: warehouse/templates/pages/help.html:251
+#: warehouse/templates/pages/help.html:308
+#: warehouse/templates/pages/help.html:335
+#: warehouse/templates/pages/help.html:340
#: warehouse/templates/pages/help.html:345
-#: warehouse/templates/pages/help.html:350
-#: warehouse/templates/pages/help.html:351
+#: warehouse/templates/pages/help.html:347
#: warehouse/templates/pages/help.html:352
-#: warehouse/templates/pages/help.html:356
-#: warehouse/templates/pages/help.html:389
+#: warehouse/templates/pages/help.html:353
+#: warehouse/templates/pages/help.html:354
+#: warehouse/templates/pages/help.html:358
#: warehouse/templates/pages/help.html:391
-#: warehouse/templates/pages/help.html:394
-#: warehouse/templates/pages/help.html:430
-#: warehouse/templates/pages/help.html:435
-#: warehouse/templates/pages/help.html:441
-#: warehouse/templates/pages/help.html:499
-#: warehouse/templates/pages/help.html:519
-#: warehouse/templates/pages/help.html:525
-#: warehouse/templates/pages/help.html:528
+#: warehouse/templates/pages/help.html:393
+#: warehouse/templates/pages/help.html:396
+#: warehouse/templates/pages/help.html:432
+#: warehouse/templates/pages/help.html:437
+#: warehouse/templates/pages/help.html:443
+#: warehouse/templates/pages/help.html:501
+#: warehouse/templates/pages/help.html:521
+#: warehouse/templates/pages/help.html:527
#: warehouse/templates/pages/help.html:530
-#: warehouse/templates/pages/help.html:539
-#: warehouse/templates/pages/help.html:551
-#: warehouse/templates/pages/help.html:558
-#: warehouse/templates/pages/help.html:570
-#: warehouse/templates/pages/help.html:571
-#: warehouse/templates/pages/help.html:576
-#: warehouse/templates/pages/help.html:601
-#: warehouse/templates/pages/help.html:614
-#: warehouse/templates/pages/help.html:619
-#: warehouse/templates/pages/help.html:631
-#: warehouse/templates/pages/help.html:652
-#: warehouse/templates/pages/help.html:675
-#: warehouse/templates/pages/help.html:682
-#: warehouse/templates/pages/help.html:694
-#: warehouse/templates/pages/help.html:705
-#: warehouse/templates/pages/help.html:710
-#: warehouse/templates/pages/help.html:718
-#: warehouse/templates/pages/help.html:729
-#: warehouse/templates/pages/help.html:746
-#: warehouse/templates/pages/help.html:753
-#: warehouse/templates/pages/help.html:761
-#: warehouse/templates/pages/help.html:777
-#: warehouse/templates/pages/help.html:782
-#: warehouse/templates/pages/help.html:787
+#: warehouse/templates/pages/help.html:532
+#: warehouse/templates/pages/help.html:541
+#: warehouse/templates/pages/help.html:553
+#: warehouse/templates/pages/help.html:560
+#: warehouse/templates/pages/help.html:572
+#: warehouse/templates/pages/help.html:573
+#: warehouse/templates/pages/help.html:578
+#: warehouse/templates/pages/help.html:603
+#: warehouse/templates/pages/help.html:616
+#: warehouse/templates/pages/help.html:621
+#: warehouse/templates/pages/help.html:633
+#: warehouse/templates/pages/help.html:654
+#: warehouse/templates/pages/help.html:677
+#: warehouse/templates/pages/help.html:684
+#: warehouse/templates/pages/help.html:696
+#: warehouse/templates/pages/help.html:707
+#: warehouse/templates/pages/help.html:712
+#: warehouse/templates/pages/help.html:720
+#: warehouse/templates/pages/help.html:731
+#: warehouse/templates/pages/help.html:748
+#: warehouse/templates/pages/help.html:768
+#: warehouse/templates/pages/help.html:776
+#: warehouse/templates/pages/help.html:792
#: warehouse/templates/pages/help.html:797
-#: warehouse/templates/pages/help.html:806
-#: warehouse/templates/pages/help.html:820
-#: warehouse/templates/pages/help.html:828
-#: warehouse/templates/pages/help.html:836
-#: warehouse/templates/pages/help.html:844
-#: warehouse/templates/pages/help.html:854
+#: warehouse/templates/pages/help.html:802
+#: warehouse/templates/pages/help.html:812
+#: warehouse/templates/pages/help.html:821
+#: warehouse/templates/pages/help.html:835
+#: warehouse/templates/pages/help.html:843
+#: warehouse/templates/pages/help.html:851
+#: warehouse/templates/pages/help.html:859
#: warehouse/templates/pages/help.html:869
#: warehouse/templates/pages/help.html:884
-#: warehouse/templates/pages/help.html:885
-#: warehouse/templates/pages/help.html:886
-#: warehouse/templates/pages/help.html:887
-#: warehouse/templates/pages/help.html:892
+#: warehouse/templates/pages/help.html:899
+#: warehouse/templates/pages/help.html:900
+#: warehouse/templates/pages/help.html:901
+#: warehouse/templates/pages/help.html:902
+#: warehouse/templates/pages/help.html:907
#: warehouse/templates/pages/security.html:36
#: warehouse/templates/pages/sponsors.html:33
#: warehouse/templates/pages/sponsors.html:37
@@ -522,7 +522,7 @@ msgstr ""
#: warehouse/templates/base.html:41 warehouse/templates/base.html:55
#: warehouse/templates/base.html:256
#: warehouse/templates/includes/current-user-indicator.html:55
-#: warehouse/templates/pages/help.html:105
+#: warehouse/templates/pages/help.html:106
#: warehouse/templates/pages/sitemap.html:27
msgid "Help"
msgstr ""
@@ -1881,7 +1881,7 @@ msgstr ""
#: warehouse/templates/includes/packaging/project-data.html:84
#: warehouse/templates/includes/packaging/project-data.html:86
-#: warehouse/templates/pages/help.html:562
+#: warehouse/templates/pages/help.html:564
msgid "Maintainer:"
msgstr ""
@@ -3437,7 +3437,7 @@ msgid ""
msgstr ""
#: warehouse/templates/manage/roles.html:39
-#: warehouse/templates/pages/help.html:561
+#: warehouse/templates/pages/help.html:563
msgid "There are two possible roles for collaborators:"
msgstr ""
@@ -4348,97 +4348,101 @@ msgid ""
msgstr ""
#: warehouse/templates/pages/help.html:89
-msgid "Why isn't my desired project name available?"
+msgid "Why am I getting an \"Invalid filename\" error?"
msgstr ""
#: warehouse/templates/pages/help.html:90
-msgid "How do I claim an abandoned or previously registered project name?"
+msgid "Why isn't my desired project name available?"
msgstr ""
#: warehouse/templates/pages/help.html:91
-msgid "What collaborator roles are available for a project on PyPI?"
+msgid "How do I claim an abandoned or previously registered project name?"
msgstr ""
#: warehouse/templates/pages/help.html:92
-msgid "How do I become an owner/maintainer of a project on PyPI?"
+msgid "What collaborator roles are available for a project on PyPI?"
msgstr ""
#: warehouse/templates/pages/help.html:93
-msgid "How can I upload a project description in a different format?"
+msgid "How do I become an owner/maintainer of a project on PyPI?"
msgstr ""
#: warehouse/templates/pages/help.html:94
-msgid "How do I request a new trove classifier?"
+msgid "How can I upload a project description in a different format?"
msgstr ""
#: warehouse/templates/pages/help.html:95
+msgid "How do I request a new trove classifier?"
+msgstr ""
+
+#: warehouse/templates/pages/help.html:96
msgid "Where can I report a bug or provide feedback about PyPI?"
msgstr ""
-#: warehouse/templates/pages/help.html:97
+#: warehouse/templates/pages/help.html:98
msgid "Who maintains PyPI?"
msgstr ""
-#: warehouse/templates/pages/help.html:98
+#: warehouse/templates/pages/help.html:99
msgid "What powers PyPI?"
msgstr ""
-#: warehouse/templates/pages/help.html:99
+#: warehouse/templates/pages/help.html:100
msgid "Can I depend on PyPI being available?"
msgstr ""
-#: warehouse/templates/pages/help.html:100
+#: warehouse/templates/pages/help.html:101
msgid "How can I contribute to PyPI?"
msgstr ""
-#: warehouse/templates/pages/help.html:101
+#: warehouse/templates/pages/help.html:102
msgid "How do I keep up with upcoming changes to PyPI?"
msgstr ""
-#: warehouse/templates/pages/help.html:102
+#: warehouse/templates/pages/help.html:103
msgid ""
"What does the \"beta feature\" badge mean? What are Warehouse's current "
"beta features?"
msgstr ""
-#: warehouse/templates/pages/help.html:103
+#: warehouse/templates/pages/help.html:104
msgid "How do I pronounce \"PyPI\"?"
msgstr ""
-#: warehouse/templates/pages/help.html:110
+#: warehouse/templates/pages/help.html:111
msgid "Common questions"
msgstr ""
-#: warehouse/templates/pages/help.html:113
-#: warehouse/templates/pages/help.html:196
+#: warehouse/templates/pages/help.html:114
+#: warehouse/templates/pages/help.html:198
msgid "Basics"
msgstr ""
-#: warehouse/templates/pages/help.html:124
+#: warehouse/templates/pages/help.html:125
msgid "My Account"
msgstr ""
-#: warehouse/templates/pages/help.html:141
-#: warehouse/templates/pages/help.html:516
+#: warehouse/templates/pages/help.html:142
+#: warehouse/templates/pages/help.html:518
msgid "Integrating"
msgstr ""
-#: warehouse/templates/pages/help.html:151
-#: warehouse/templates/pages/help.html:543
+#: warehouse/templates/pages/help.html:152
+#: warehouse/templates/pages/help.html:545
msgid "Administration of projects on PyPI"
msgstr ""
-#: warehouse/templates/pages/help.html:166
-#: warehouse/templates/pages/help.html:627
+#: warehouse/templates/pages/help.html:167
+#: warehouse/templates/pages/help.html:629
msgid "Troubleshooting"
msgstr ""
-#: warehouse/templates/pages/help.html:183
-#: warehouse/templates/pages/help.html:773
+#: warehouse/templates/pages/help.html:185
+#: warehouse/templates/pages/help.html:788
msgid "About"
msgstr ""
-#: warehouse/templates/pages/help.html:199
+#: warehouse/templates/pages/help.html:201
#, python-format
msgid ""
"\n"
@@ -4462,7 +4466,7 @@ msgid ""
" "
msgstr ""
-#: warehouse/templates/pages/help.html:208
+#: warehouse/templates/pages/help.html:210
#, python-format
msgid ""
"To learn how to install a file from PyPI, visit the Python Packaging User Guide."
msgstr ""
-#: warehouse/templates/pages/help.html:215
+#: warehouse/templates/pages/help.html:217
#, python-format
msgid ""
"For full instructions on configuring, packaging and distributing your "
@@ -4482,7 +4486,7 @@ msgid ""
"target=\"_blank\" rel=\"noopener\">Python Packaging User Guide."
msgstr ""
-#: warehouse/templates/pages/help.html:222
+#: warehouse/templates/pages/help.html:224
#, python-format
msgid ""
"Classifiers are used to categorize projects on PyPI. See PyPI itself has not suffered a breach. This is a protective measure "
@@ -4570,7 +4574,7 @@ msgid ""
"href=\"%(reset_pwd_href)s\">reset your password.
"
msgstr ""
-#: warehouse/templates/pages/help.html:284
+#: warehouse/templates/pages/help.html:286
#, python-format
msgid ""
" All PyPI user events are stored under security history in account "
@@ -4580,7 +4584,7 @@ msgid ""
"href=\"mailto:%(admin_email)s\">%(admin_email)s
"
msgstr ""
-#: warehouse/templates/pages/help.html:296
+#: warehouse/templates/pages/help.html:298
msgid ""
" A PyPI API token linked to your account was posted on a public "
"website. It was automatically revoked, but before regenerating a new one,"
@@ -4589,7 +4593,7 @@ msgid ""
"applies too.
"
msgstr ""
-#: warehouse/templates/pages/help.html:306
+#: warehouse/templates/pages/help.html:308
#, python-format
msgid ""
" Two factor authentication (2FA) makes your account more secure by "
@@ -4608,7 +4612,7 @@ msgid ""
"rel=\"noopener\">discuss.python.org.
"
msgstr ""
-#: warehouse/templates/pages/help.html:333
+#: warehouse/templates/pages/help.html:335
#, python-format
msgid ""
"PyPI users can set up two-factor authentication using any authentication "
@@ -4617,21 +4621,21 @@ msgid ""
"password\">TOTP standard."
msgstr ""
-#: warehouse/templates/pages/help.html:334
+#: warehouse/templates/pages/help.html:336
msgid ""
"TOTP authentication "
"applications generate a regularly changing authentication code to use "
"when logging into your account."
msgstr ""
-#: warehouse/templates/pages/help.html:335
+#: warehouse/templates/pages/help.html:337
msgid ""
"Because TOTP is an "
"open standard, there are many applications that are compatible with your "
"PyPI account. Popular applications include:"
msgstr ""
-#: warehouse/templates/pages/help.html:338
+#: warehouse/templates/pages/help.html:340
#, python-format
msgid ""
"Google Authenticator for iOS"
msgstr ""
-#: warehouse/templates/pages/help.html:341
#: warehouse/templates/pages/help.html:343
-#: warehouse/templates/pages/help.html:348
+#: warehouse/templates/pages/help.html:345
#: warehouse/templates/pages/help.html:350
+#: warehouse/templates/pages/help.html:352
msgid "(proprietary)"
msgstr ""
-#: warehouse/templates/pages/help.html:345
+#: warehouse/templates/pages/help.html:347
#, python-format
msgid ""
"Duo Mobile for iOS"
msgstr ""
-#: warehouse/templates/pages/help.html:351
-#: warehouse/templates/pages/help.html:352
+#: warehouse/templates/pages/help.html:353
+#: warehouse/templates/pages/help.html:354
msgid "(open source)"
msgstr ""
-#: warehouse/templates/pages/help.html:356
+#: warehouse/templates/pages/help.html:358
#, python-format
msgid ""
"Some password managers (e.g. 2FA with an "
"authentication application:"
msgstr ""
-#: warehouse/templates/pages/help.html:366
+#: warehouse/templates/pages/help.html:368
msgid ""
"Open an authentication (TOTP) application"
msgstr ""
-#: warehouse/templates/pages/help.html:367
+#: warehouse/templates/pages/help.html:369
msgid ""
"Log in to your PyPI account, go to your account settings, and choose "
"\"Add 2FA with "
"authentication application\""
msgstr ""
-#: warehouse/templates/pages/help.html:368
+#: warehouse/templates/pages/help.html:370
msgid ""
"PyPI will generate a secret key, specific to your account. This is "
"displayed as a QR code, and as a text code."
msgstr ""
-#: warehouse/templates/pages/help.html:369
+#: warehouse/templates/pages/help.html:371
msgid ""
"Scan the QR code with your authentication application, or type it in "
"manually. The method of input will depend on the application you have "
"chosen."
msgstr ""
-#: warehouse/templates/pages/help.html:370
+#: warehouse/templates/pages/help.html:372
msgid ""
"Your application will generate an authentication code - use this to "
"verify your set up on PyPI"
msgstr ""
-#: warehouse/templates/pages/help.html:373
+#: warehouse/templates/pages/help.html:375
msgid ""
"The PyPI server and your application now share your PyPI secret key, "
"allowing your application to generate valid authentication codes for your"
" PyPI account."
msgstr ""
-#: warehouse/templates/pages/help.html:375
-#: warehouse/templates/pages/help.html:417
+#: warehouse/templates/pages/help.html:377
+#: warehouse/templates/pages/help.html:419
msgid "Next time you log in to PyPI you'll need to:"
msgstr ""
-#: warehouse/templates/pages/help.html:377
-#: warehouse/templates/pages/help.html:469
+#: warehouse/templates/pages/help.html:379
+#: warehouse/templates/pages/help.html:471
msgid "Provide your username and password, as normal"
msgstr ""
-#: warehouse/templates/pages/help.html:378
+#: warehouse/templates/pages/help.html:380
msgid "Open your authentication application to generate an authentication code"
msgstr ""
-#: warehouse/templates/pages/help.html:379
+#: warehouse/templates/pages/help.html:381
msgid "Use this code to finish logging into PyPI"
msgstr ""
-#: warehouse/templates/pages/help.html:385
+#: warehouse/templates/pages/help.html:387
msgid ""
"A security device is a USB key or other "
"device that generates a one-time password and sends that password to "
@@ -4741,11 +4745,11 @@ msgid ""
"user."
msgstr ""
-#: warehouse/templates/pages/help.html:387
+#: warehouse/templates/pages/help.html:389
msgid "To set up two factor authentication with a USB key, you'll need:"
msgstr ""
-#: warehouse/templates/pages/help.html:389
+#: warehouse/templates/pages/help.html:391
#, python-format
msgid ""
"To use a :"
msgstr ""
-#: warehouse/templates/pages/help.html:394
+#: warehouse/templates/pages/help.html:396
#, python-format
msgid ""
"Popular keys include Thetis."
msgstr ""
-#: warehouse/templates/pages/help.html:401
+#: warehouse/templates/pages/help.html:403
msgid ""
"Note that some older Yubico USB keys do not follow the FIDO "
"specification, and will therefore not work with PyPI"
msgstr ""
-#: warehouse/templates/pages/help.html:406
+#: warehouse/templates/pages/help.html:408
msgid "Follow these steps:"
msgstr ""
-#: warehouse/templates/pages/help.html:408
+#: warehouse/templates/pages/help.html:410
msgid ""
"\n"
" Log in to your PyPI account, go to your account settings, "
@@ -4800,13 +4804,13 @@ msgid ""
" "
msgstr ""
-#: warehouse/templates/pages/help.html:415
+#: warehouse/templates/pages/help.html:417
msgid ""
"Once complete, your USB key will be registered to your PyPI account and "
"can be used during the log in process."
msgstr ""
-#: warehouse/templates/pages/help.html:419
+#: warehouse/templates/pages/help.html:421
msgid ""
"\n"
" Provide your username and password, as normal\n"
@@ -4815,7 +4819,7 @@ msgid ""
" "
msgstr ""
-#: warehouse/templates/pages/help.html:430
+#: warehouse/templates/pages/help.html:432
#, python-format
msgid ""
"There is a growing ecosystem of mobile phones to act as security devices."
msgstr ""
-#: warehouse/templates/pages/help.html:441
+#: warehouse/templates/pages/help.html:443
#, python-format
msgid ""
"As PyPI's two factor implementation follows the authentication "
"application or security device, you can use "
"these codes to sign into PyPI."
msgstr ""
-#: warehouse/templates/pages/help.html:453
+#: warehouse/templates/pages/help.html:455
msgid ""
"Recovery codes are one time use. They are not a "
"substitute for a authentication application or API tokens provide an alternative way (instead of username "
@@ -4917,41 +4921,41 @@ msgid ""
" "
msgstr ""
-#: warehouse/templates/pages/help.html:484
+#: warehouse/templates/pages/help.html:486
msgid "To make an API token:"
msgstr ""
-#: warehouse/templates/pages/help.html:487
+#: warehouse/templates/pages/help.html:489
msgid "Verify your email address"
msgstr ""
-#: warehouse/templates/pages/help.html:487
+#: warehouse/templates/pages/help.html:489
#, python-format
msgid "(check your account settings)"
msgstr ""
-#: warehouse/templates/pages/help.html:488
+#: warehouse/templates/pages/help.html:490
#, python-format
msgid ""
"In your account settings, go to the API tokens "
"section and select \"Add API token\""
msgstr ""
-#: warehouse/templates/pages/help.html:491
+#: warehouse/templates/pages/help.html:493
msgid "To use an API token:"
msgstr ""
-#: warehouse/templates/pages/help.html:494
+#: warehouse/templates/pages/help.html:496
msgid "Set your username to __token__
"
msgstr ""
-#: warehouse/templates/pages/help.html:495
+#: warehouse/templates/pages/help.html:497
msgid ""
"Set your password to the token value, including the pypi-
"
"prefix"
msgstr ""
-#: warehouse/templates/pages/help.html:499
+#: warehouse/templates/pages/help.html:501
#, python-format
msgid ""
"Where you edit or add these values will depend on your individual use "
@@ -4963,14 +4967,14 @@ msgid ""
"rel=\"noopener\">.travis.yml
if you are using Travis)."
msgstr ""
-#: warehouse/templates/pages/help.html:503
+#: warehouse/templates/pages/help.html:505
msgid ""
"Advanced users may wish to inspect their token by decoding it with "
"base64, and checking the output against the unique identifier displayed "
"on PyPI."
msgstr ""
-#: warehouse/templates/pages/help.html:507
+#: warehouse/templates/pages/help.html:509
msgid ""
"\n"
" PyPI asks you to confirm your password before you want to "
@@ -4984,15 +4988,15 @@ msgid ""
" "
msgstr ""
-#: warehouse/templates/pages/help.html:519
+#: warehouse/templates/pages/help.html:521
msgid "Yes, including RSS feeds of new packages and new releases."
msgstr ""
-#: warehouse/templates/pages/help.html:519
+#: warehouse/templates/pages/help.html:521
msgid "See the API reference."
msgstr ""
-#: warehouse/templates/pages/help.html:522
+#: warehouse/templates/pages/help.html:524
#, python-format
msgid ""
"If you need to run your own mirror of PyPI, the GitHub apps."
msgstr ""
-#: warehouse/templates/pages/help.html:528
+#: warehouse/templates/pages/help.html:530
#, python-format
msgid ""
"You can analyze PyPI project/package metadata and via our public dataset on Google BigQuery."
msgstr ""
-#: warehouse/templates/pages/help.html:530
+#: warehouse/templates/pages/help.html:532
#, python-format
msgid ""
"other relevant factors."
msgstr ""
-#: warehouse/templates/pages/help.html:539
+#: warehouse/templates/pages/help.html:541
#, python-format
msgid ""
"For recent statistics on uptime and performance, see ."
msgstr ""
-#: warehouse/templates/pages/help.html:546
+#: warehouse/templates/pages/help.html:548
#, python-format
msgid ""
"PyPI does not support publishing private packages. If you need to publish"
@@ -5051,7 +5055,7 @@ msgid ""
"run your own deployment of the devpi project."
msgstr ""
-#: warehouse/templates/pages/help.html:549
+#: warehouse/templates/pages/help.html:551
msgid ""
"Your publishing tool may return an error that your new project can't be "
"created with your desired name, despite no evidence of a project or "
@@ -5059,7 +5063,7 @@ msgid ""
"reasons this may occur:"
msgstr ""
-#: warehouse/templates/pages/help.html:551
+#: warehouse/templates/pages/help.html:553
#, python-format
msgid ""
"The project name conflicts with a module from any major version from 2.5 to present."
msgstr ""
-#: warehouse/templates/pages/help.html:552
+#: warehouse/templates/pages/help.html:554
msgid ""
"The project name is too similar to an existing project and may be "
"confusable."
msgstr ""
-#: warehouse/templates/pages/help.html:553
+#: warehouse/templates/pages/help.html:555
#, python-format
msgid ""
"The project name has been explicitly prohibited by the PyPI "
@@ -5082,18 +5086,18 @@ msgid ""
"with a malicious package."
msgstr ""
-#: warehouse/templates/pages/help.html:554
+#: warehouse/templates/pages/help.html:556
msgid ""
"The project name has been registered by another user, but no releases "
"have been created."
msgstr ""
-#: warehouse/templates/pages/help.html:554
+#: warehouse/templates/pages/help.html:556
#, python-format
msgid "See %(anchor_text)s"
msgstr ""
-#: warehouse/templates/pages/help.html:558
+#: warehouse/templates/pages/help.html:560
#, python-format
msgid ""
"Follow the PEP 541."
msgstr ""
-#: warehouse/templates/pages/help.html:562
+#: warehouse/templates/pages/help.html:564
msgid ""
"Can upload releases for a package. Cannot add collaborators. Cannot "
"delete files, releases, or the project."
msgstr ""
-#: warehouse/templates/pages/help.html:563
+#: warehouse/templates/pages/help.html:565
msgid "Owner:"
msgstr ""
-#: warehouse/templates/pages/help.html:563
+#: warehouse/templates/pages/help.html:565
msgid ""
"Can upload releases. Can add other collaborators. Can delete files, "
"releases, or the entire project."
msgstr ""
-#: warehouse/templates/pages/help.html:566
+#: warehouse/templates/pages/help.html:568
msgid ""
"Only the current owners of a project have the ability to add new owners "
"or maintainers. If you need to request ownership, you should contact the "
@@ -5126,12 +5130,12 @@ msgid ""
"project page."
msgstr ""
-#: warehouse/templates/pages/help.html:567
+#: warehouse/templates/pages/help.html:569
#, python-format
msgid "If the owner is unresponsive, see %(anchor_text)s"
msgstr ""
-#: warehouse/templates/pages/help.html:570
+#: warehouse/templates/pages/help.html:572
#, python-format
msgid ""
"By default, an upload's description will render with file an issue and tell us:"
msgstr ""
-#: warehouse/templates/pages/help.html:585
-#: warehouse/templates/pages/help.html:606
+#: warehouse/templates/pages/help.html:587
+#: warehouse/templates/pages/help.html:608
msgid "A link to your project on PyPI (or Test PyPI)"
msgstr ""
-#: warehouse/templates/pages/help.html:586
+#: warehouse/templates/pages/help.html:588
msgid "The size of your release, in megabytes"
msgstr ""
-#: warehouse/templates/pages/help.html:587
+#: warehouse/templates/pages/help.html:589
msgid "Which index/indexes you need the increase for (PyPI, Test PyPI, or both)"
msgstr ""
-#: warehouse/templates/pages/help.html:588
-#: warehouse/templates/pages/help.html:608
+#: warehouse/templates/pages/help.html:590
+#: warehouse/templates/pages/help.html:610
msgid ""
"A brief description of your project, including the reason for the "
"additional size."
msgstr ""
-#: warehouse/templates/pages/help.html:594
+#: warehouse/templates/pages/help.html:596
msgid ""
"If you can't upload your project's release to PyPI because you're hitting"
" the project size limit, first remove any unnecessary releases or "
"individual files to lower your overall project size."
msgstr ""
-#: warehouse/templates/pages/help.html:601
+#: warehouse/templates/pages/help.html:603
#, python-format
msgid ""
"If that is not possible, we can sometimes increase your limit. File an issue and tell us:"
msgstr ""
-#: warehouse/templates/pages/help.html:607
+#: warehouse/templates/pages/help.html:609
msgid "The total size of your project, in gigabytes"
msgstr ""
-#: warehouse/templates/pages/help.html:614
+#: warehouse/templates/pages/help.html:616
#, python-format
msgid ""
"PyPI receives reports on vulnerabilities in the packages hosted on it "
@@ -5213,7 +5217,7 @@ msgid ""
"Advisory Database."
msgstr ""
-#: warehouse/templates/pages/help.html:619
+#: warehouse/templates/pages/help.html:621
#, python-format
msgid ""
"If you believe vulnerability data for your project is invalid or "
@@ -5221,7 +5225,7 @@ msgid ""
"target=\"_blank\" rel=\"noopener\">file an issue with details."
msgstr ""
-#: warehouse/templates/pages/help.html:631
+#: warehouse/templates/pages/help.html:633
#, python-format
msgid ""
"PyPI will reject uploads if the package description fails to render. You "
@@ -5229,41 +5233,41 @@ msgid ""
"command to locally check a description for validity."
msgstr ""
-#: warehouse/templates/pages/help.html:637
+#: warehouse/templates/pages/help.html:639
msgid ""
"If you've forgotten your PyPI password but you remember your email "
"address or username, follow these steps to reset your password:"
msgstr ""
-#: warehouse/templates/pages/help.html:639
+#: warehouse/templates/pages/help.html:641
#, python-format
msgid "Go to reset your password."
msgstr ""
-#: warehouse/templates/pages/help.html:640
+#: warehouse/templates/pages/help.html:642
msgid "Enter the email address or username you used for PyPI and submit the form."
msgstr ""
-#: warehouse/templates/pages/help.html:641
+#: warehouse/templates/pages/help.html:643
msgid "You'll receive an email with a password reset link."
msgstr ""
-#: warehouse/templates/pages/help.html:646
+#: warehouse/templates/pages/help.html:648
msgid "If you've lost access to your PyPI account due to:"
msgstr ""
-#: warehouse/templates/pages/help.html:648
+#: warehouse/templates/pages/help.html:650
msgid "Lost access to the email address associated with your account"
msgstr ""
-#: warehouse/templates/pages/help.html:649
+#: warehouse/templates/pages/help.html:651
msgid ""
"Lost two factor authentication application, device, and recovery "
"codes"
msgstr ""
-#: warehouse/templates/pages/help.html:652
+#: warehouse/templates/pages/help.html:654
#, python-format
msgid ""
"You can proceed to API Token for uploads:"
msgstr ""
-#: warehouse/templates/pages/help.html:666
+#: warehouse/templates/pages/help.html:668
msgid "Ensure that your API Token is valid and has not been revoked."
msgstr ""
-#: warehouse/templates/pages/help.html:667
+#: warehouse/templates/pages/help.html:669
msgid ""
"Ensure that your API Token is properly "
"formatted and does not contain any trailing characters such as "
"newlines."
msgstr ""
-#: warehouse/templates/pages/help.html:669
+#: warehouse/templates/pages/help.html:671
msgid ""
"In both cases, remember that PyPI and TestPyPI each require you to create"
" an account, so your credentials may be different."
msgstr ""
-#: warehouse/templates/pages/help.html:671
+#: warehouse/templates/pages/help.html:673
msgid ""
"\n"
" If you're using Windows and trying to paste your password or "
@@ -5317,7 +5321,7 @@ msgid ""
" "
msgstr ""
-#: warehouse/templates/pages/help.html:675
+#: warehouse/templates/pages/help.html:677
#, python-format
msgid ""
"This is a Learn why on the PSF blog."
msgstr ""
-#: warehouse/templates/pages/help.html:689
+#: warehouse/templates/pages/help.html:691
#, python-format
msgid ""
"If you are having trouble with %(command)s
and get a "
@@ -5346,7 +5350,7 @@ msgid ""
"information:"
msgstr ""
-#: warehouse/templates/pages/help.html:691
+#: warehouse/templates/pages/help.html:693
msgid ""
"If you see an error like There was a problem confirming the ssl "
"certificate
or tlsv1 alert protocol version
or "
@@ -5354,7 +5358,7 @@ msgid ""
"PyPI with a newer TLS support library."
msgstr ""
-#: warehouse/templates/pages/help.html:692
+#: warehouse/templates/pages/help.html:694
msgid ""
"The specific steps you need to take will depend on your operating system "
"version, where your installation of Python originated (python.org, your "
@@ -5362,7 +5366,7 @@ msgid ""
" Python, setuptools
, and pip
."
msgstr ""
-#: warehouse/templates/pages/help.html:694
+#: warehouse/templates/pages/help.html:696
#, python-format
msgid ""
"For help, go to %(command)s."
msgstr ""
-#: warehouse/templates/pages/help.html:705
+#: warehouse/templates/pages/help.html:707
#, python-format
msgid ""
"We take , so we can try to fix the problem, for you and others."
msgstr ""
-#: warehouse/templates/pages/help.html:718
+#: warehouse/templates/pages/help.html:720
#, python-format
msgid ""
"In a previous version of PyPI, it used to be possible for maintainers to "
@@ -5401,7 +5405,7 @@ msgid ""
"rel=\"noopener\">use twine to upload your project to PyPI."
msgstr ""
-#: warehouse/templates/pages/help.html:727
+#: warehouse/templates/pages/help.html:729
msgid ""
"Spammers return to PyPI with some regularity hoping to place their Search"
" Engine Optimized phishing, scam, and click-farming content on the site. "
@@ -5410,7 +5414,7 @@ msgid ""
"prime target."
msgstr ""
-#: warehouse/templates/pages/help.html:729
+#: warehouse/templates/pages/help.html:731
#, python-format
msgid ""
"When the PyPI administrators are overwhelmed by spam or "
@@ -5421,29 +5425,29 @@ msgid ""
"have updated it with reasoning for the intervention."
msgstr ""
-#: warehouse/templates/pages/help.html:738
+#: warehouse/templates/pages/help.html:740
msgid "PyPI will return these errors for one of these reasons:"
msgstr ""
-#: warehouse/templates/pages/help.html:740
+#: warehouse/templates/pages/help.html:742
msgid "Filename has been used and file exists"
msgstr ""
-#: warehouse/templates/pages/help.html:741
+#: warehouse/templates/pages/help.html:743
msgid "Filename has been used but file no longer exists"
msgstr ""
-#: warehouse/templates/pages/help.html:742
+#: warehouse/templates/pages/help.html:744
msgid "A file with the exact same content exists"
msgstr ""
-#: warehouse/templates/pages/help.html:744
+#: warehouse/templates/pages/help.html:746
msgid ""
"PyPI does not allow for a filename to be reused, even once a project has "
"been deleted and recreated."
msgstr ""
-#: warehouse/templates/pages/help.html:746
+#: warehouse/templates/pages/help.html:748
#, python-format
msgid ""
"To avoid this situation, pypi.org."
msgstr ""
-#: warehouse/templates/pages/help.html:753
+#: warehouse/templates/pages/help.html:754
+msgid ""
+"Package files uploaded to PyPI need to have names in a standard format. "
+"Your packaging tools should create files with suitable names by default. "
+"The filenames consist of a number of parts, separated by hyphens "
+"(-
):"
+msgstr ""
+
+#: warehouse/templates/pages/help.html:756
+msgid ""
+"For source distributions: normalized project name and version, e.g. "
+"importlib_metadata-4.10.1.tar.gz
"
+msgstr ""
+
+#: warehouse/templates/pages/help.html:757
+msgid ""
+"For wheels: normalized project name, version, build tag (optional), "
+"Python tag, ABI tag, and platform tag, e.g. "
+"importlib_metadata-4.10.1-py3-none-any.whl
"
+msgstr ""
+
+#: warehouse/templates/pages/help.html:760
+#, python-format
+msgid ""
+"A normalized project name must be fully lower-cased, with any runs of "
+"_-.
characters replaced with a single underscore "
+"(_
). The version number should be normalized as described in"
+" PEP 440. All parts are expected to consist "
+"only of ASCII characters."
+msgstr ""
+
+#: warehouse/templates/pages/help.html:764
+msgid ""
+"These rules have become stricter over time, so you may see existing "
+"packages with names which would no longer be allowed for new uploads."
+msgstr ""
+
+#: warehouse/templates/pages/help.html:768
#, python-format
msgid ""
"If you would like to request a new trove classifier file a pull request "
@@ -5461,7 +5502,7 @@ msgid ""
" to include a brief justification of why it is important."
msgstr ""
-#: warehouse/templates/pages/help.html:761
+#: warehouse/templates/pages/help.html:776
#, python-format
msgid ""
"If you're experiencing an issue with PyPI itself, we welcome "
@@ -5472,14 +5513,14 @@ msgid ""
" first check that a similar issue does not already exist."
msgstr ""
-#: warehouse/templates/pages/help.html:768
+#: warehouse/templates/pages/help.html:783
msgid ""
"If you are having an issue is with a specific package installed from "
"PyPI, you should reach out to the maintainers of that project directly "
"instead."
msgstr ""
-#: warehouse/templates/pages/help.html:777
+#: warehouse/templates/pages/help.html:792
#, python-format
msgid ""
"PyPI is powered by the Warehouse project; ."
msgstr ""
-#: warehouse/templates/pages/help.html:804
+#: warehouse/templates/pages/help.html:819
msgid ""
"As of April 16, 2018, PyPI.org is at \"production\" status, meaning that "
"it has moved out of beta and replaced the old site (pypi.python.org). It "
"is now robust, tested, and ready for expected browser and API traffic."
msgstr ""
-#: warehouse/templates/pages/help.html:806
+#: warehouse/templates/pages/help.html:821
#, python-format
msgid ""
"PyPI is heavily cached and distributed via private index."
msgstr ""
-#: warehouse/templates/pages/help.html:820
+#: warehouse/templates/pages/help.html:835
#, python-format
msgid ""
"We have a huge amount of work to do to continue to maintain and improve "
@@ -5552,22 +5593,22 @@ msgid ""
"target=\"_blank\" rel=\"noopener\">the Warehouse project)."
msgstr ""
-#: warehouse/templates/pages/help.html:825
+#: warehouse/templates/pages/help.html:840
msgid "Financial:"
msgstr ""
-#: warehouse/templates/pages/help.html:825
+#: warehouse/templates/pages/help.html:840
#, python-format
msgid ""
"We would deeply appreciate your donations to fund "
"development and maintenance."
msgstr ""
-#: warehouse/templates/pages/help.html:826
+#: warehouse/templates/pages/help.html:841
msgid "Development:"
msgstr ""
-#: warehouse/templates/pages/help.html:826
+#: warehouse/templates/pages/help.html:841
msgid ""
"Warehouse is open source, and we would love to see some new faces working"
" on the project. You do not need to be an experienced "
@@ -5575,7 +5616,7 @@ msgid ""
" you make your first open source pull request!"
msgstr ""
-#: warehouse/templates/pages/help.html:828
+#: warehouse/templates/pages/help.html:843
#, python-format
msgid ""
"If you have skills in Python, ElasticSearch, HTML, SCSS, JavaScript, or "
@@ -5589,7 +5630,7 @@ msgid ""
"here."
msgstr ""
-#: warehouse/templates/pages/help.html:836
+#: warehouse/templates/pages/help.html:851
#, python-format
msgid ""
"Issues are grouped into Python packaging forum on Discourse."
msgstr ""
-#: warehouse/templates/pages/help.html:854
+#: warehouse/templates/pages/help.html:869
#, python-format
msgid ""
"Changes to PyPI are generally announced on both the {% trans %}Troubleshooting{% endtrans %}
{{ accessibility() }}
{{ admin_intervention() }}
{{ file_name_reuse() }}
+ {{ invalid_file_name() }}
{{ new_classifier() }}
{{ feedback() }}
@@ -748,6 +750,19 @@ {{ file_name_reuse() }}
{% endtrans %}
+ {{ invalid_file_name() }}
+ {% trans %}Package files uploaded to PyPI need to have names in a standard format. Your packaging tools should create files with suitable names by default. The filenames consist of a number of parts, separated by hyphens (-
):{% endtrans %}
+
+ - {% trans %}For source distributions: normalized project name and version, e.g.
importlib_metadata-4.10.1.tar.gz
{% endtrans %}
+ - {% trans %}For wheels: normalized project name, version, build tag (optional), Python tag, ABI tag, and platform tag, e.g.
importlib_metadata-4.10.1-py3-none-any.whl
{% endtrans %}
+
+
+ {% trans trimmed pep440="https://peps.python.org/pep-0440/" %}
+ A normalized project name must be fully lower-cased, with any runs of _-.
characters replaced with a single underscore (_
). The version number should be normalized as described in PEP 440. All parts are expected to consist only of ASCII characters.
+ {% endtrans %}
+
+ {% trans %}These rules have become stricter over time, so you may see existing packages with names which would no longer be allowed for new uploads.{% endtrans %}
+
{{ new_classifier() }}
{% trans trimmed href='https://github.com/pypa/trove-classifiers/', title=gettext('External link') %}