We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Classifiers: Python version sort order
<!--
    NOTE: This issue should be for problems with PyPI itself, including:
    * pypi.org
    * test.pypi.org
    * files.pythonhosted.org

    This issue should NOT be for a project installed from PyPI. If you are
    having an issue with a specific package, you should reach out to the
    maintainers of that project directly instead.

    Furthermore, this issue should NOT be for any non-PyPI properties (like
    python.org, docs.python.org, etc.)
-->

**Describe the bug**
<!-- A clear and concise description the bug -->

The classifiers "Programming Language :: Python :: 3.X" aren't sorted in the right order on https://pypi.org as well as on https://test.pypi.org

I'm defining the classifiers like this in the `setup.py` file.

```
classifiers=[
    "Programming Language :: Python :: 3",
    "Programming Language :: Python :: 3.6",
    "Programming Language :: Python :: 3.7",
    "Programming Language :: Python :: 3.8",
    "Programming Language :: Python :: 3.9",
    "Programming Language :: Python :: 3.10"
    ]
```

In the navigation bar on pypy.org it will then appear like this: 
![image](https://user-images.githubusercontent.com/70264417/99465712-42797f00-293b-11eb-8f1a-dced842f433f.png)

With Python 3.10 at the top instead of at the bottom (after Python 3.9).


To give the visitors of pypi.org a better and faster overview over a project, it would be great if the Python classifiers were sorted by the Python versions.


**Expected behavior**
<!-- A clear and concise description of what you expected to happen -->

Classifiers sorted by Python versions.

Python :: 3
Python :: 3.6
Python :: 3.7
Python :: 3.8
Python :: 3.9
Python :: 3.10
Python :: 3.11
Python :: 3.12
etc.

**To Reproduce**
<!-- Steps to reproduce the bug, or a link to PyPI where the bug is visible -->

It can be seen for example here: https://pypi.org/project/officeextractor/


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `warehouse/filters.py`
Content:
```
1 # Licensed under the Apache License, Version 2.0 (the "License");
2 # you may not use this file except in compliance with the License.
3 # You may obtain a copy of the License at
4 #
5 # http://www.apache.org/licenses/LICENSE-2.0
6 #
7 # Unless required by applicable law or agreed to in writing, software
8 # distributed under the License is distributed on an "AS IS" BASIS,
9 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10 # See the License for the specific language governing permissions and
11 # limitations under the License.
12 
13 import binascii
14 import collections
15 import datetime
16 import enum
17 import hmac
18 import json
19 import re
20 import urllib.parse
21 
22 from email.utils import getaddresses
23 
24 import html5lib
25 import html5lib.serializer
26 import html5lib.treewalkers
27 import jinja2
28 import packaging.version
29 import pytz
30 
31 from pyramid.threadlocal import get_current_request
32 
33 from warehouse.utils.http import is_valid_uri
34 
35 
36 class PackageType(enum.Enum):
37     bdist_dmg = "OSX Disk Image"
38     bdist_dumb = "Dumb Binary"
39     bdist_egg = "Egg"
40     bdist_msi = "Windows MSI Installer"
41     bdist_rpm = "RPM"
42     bdist_wheel = "Wheel"
43     bdist_wininst = "Windows Installer"
44     sdist = "Source"
45 
46 
47 def format_package_type(value):
48     try:
49         return PackageType[value].value
50     except KeyError:
51         return value
52 
53 
54 def _camo_url(request, url):
55     camo_url = request.registry.settings["camo.url"].format(request=request)
56     camo_key = request.registry.settings["camo.key"].encode("utf8")
57     url = url.encode("utf8")
58 
59     path = "/".join(
60         [
61             hmac.new(camo_key, url, digestmod="sha1").hexdigest(),
62             binascii.hexlify(url).decode("utf8"),
63         ]
64     )
65 
66     return urllib.parse.urljoin(camo_url, path)
67 
68 
69 @jinja2.pass_context
70 def camoify(ctx, value):
71     request = ctx.get("request") or get_current_request()
72 
73     # Parse the rendered output and replace any inline images that don't point
74     # to HTTPS with camouflaged images.
75     tree_builder = html5lib.treebuilders.getTreeBuilder("dom")
76     parser = html5lib.html5parser.HTMLParser(tree=tree_builder)
77     dom = parser.parse(value)
78 
79     for element in dom.getElementsByTagName("img"):
80         src = element.getAttribute("src")
81         if src:
82             element.setAttribute("src", request.camo_url(src))
83 
84     tree_walker = html5lib.treewalkers.getTreeWalker("dom")
85     html_serializer = html5lib.serializer.HTMLSerializer()
86     camoed = "".join(html_serializer.serialize(tree_walker(dom)))
87 
88     return camoed
89 
90 
91 _SI_SYMBOLS = ["k", "M", "G", "T", "P", "E", "Z", "Y"]
92 
93 
94 def shorten_number(value):
95     for i, symbol in enumerate(_SI_SYMBOLS):
96         magnitude = value / (1000 ** (i + 1))
97         if magnitude >= 1 and magnitude < 1000:
98             return "{:.3g}{}".format(magnitude, symbol)
99 
100     return str(value)
101 
102 
103 def tojson(value):
104     return json.dumps(value, sort_keys=True, separators=(",", ":"))
105 
106 
107 def urlparse(value):
108     return urllib.parse.urlparse(value)
109 
110 
111 def format_tags(tags):
112     # split tags
113     if re.search(r",", tags):
114         split_tags = re.split(r"\s*,\s*", tags)
115     elif re.search(r";", tags):
116         split_tags = re.split(r"\s*;\s*", tags)
117     else:
118         split_tags = re.split(r"\s+", tags)
119 
120     # strip whitespace, quotes, double quotes
121     stripped_tags = [re.sub(r'^["\'\s]+|["\'\s]+$', "", t) for t in split_tags]
122 
123     # remove any empty tags
124     formatted_tags = [t for t in stripped_tags if t]
125 
126     return formatted_tags
127 
128 
129 def format_classifiers(classifiers):
130     structured = collections.OrderedDict()
131 
132     # Split up our classifiers into our data structure
133     for classifier in classifiers:
134         key, *value = classifier.split(" :: ", 1)
135         if value:
136             if key not in structured:
137                 structured[key] = []
138             structured[key].append(value[0])
139 
140     return structured
141 
142 
143 def classifier_id(classifier):
144     return classifier.replace(" ", "_").replace("::", ".")
145 
146 
147 def contains_valid_uris(items):
148     """Returns boolean representing whether the input list contains any valid
149     URIs
150     """
151     return any(is_valid_uri(i) for i in items)
152 
153 
154 def parse_version(version_str):
155     return packaging.version.parse(version_str)
156 
157 
158 def localize_datetime(timestamp):
159     return pytz.utc.localize(timestamp)
160 
161 
162 def ctime(timestamp):
163     return datetime.datetime.fromtimestamp(timestamp)
164 
165 
166 def is_recent(timestamp):
167     if timestamp:
168         return timestamp + datetime.timedelta(days=30) > datetime.datetime.now()
169     return False
170 
171 
172 def format_author_email(metadata_email: str) -> tuple[str, str]:
173     """
174     Return the name and email address from a metadata RFC-822 string.
175     Use Jinja's `first` and `last` to access each part in a template.
176     TODO: Support more than one email address, per RFC-822.
177     """
178     author_emails = []
179     for author_name, author_email in getaddresses([metadata_email]):
180         if "@" not in author_email:
181             return author_name, ""
182         author_emails.append((author_name, author_email))
183     return author_emails[0][0], author_emails[0][1]
184 
185 
186 def includeme(config):
187     config.add_request_method(_camo_url, name="camo_url")
188
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/warehouse/filters.py b/warehouse/filters.py
--- a/warehouse/filters.py
+++ b/warehouse/filters.py
@@ -28,6 +28,7 @@
 import packaging.version
 import pytz
 
+from natsort import natsorted
 from pyramid.threadlocal import get_current_request
 
 from warehouse.utils.http import is_valid_uri
@@ -137,6 +138,10 @@
                 structured[key] = []
             structured[key].append(value[0])
 
+    # Sort all the values in our data structure
+    for key, value in structured.items():
+        structured[key] = natsorted(value)
+
     return structured

{"golden_diff": "diff --git a/warehouse/filters.py b/warehouse/filters.py\n--- a/warehouse/filters.py\n+++ b/warehouse/filters.py\n@@ -28,6 +28,7 @@\n import packaging.version\n import pytz\n \n+from natsort import natsorted\n from pyramid.threadlocal import get_current_request\n \n from warehouse.utils.http import is_valid_uri\n@@ -137,6 +138,10 @@\n                 structured[key] = []\n             structured[key].append(value[0])\n \n+    # Sort all the values in our data structure\n+    for key, value in structured.items():\n+        structured[key] = natsorted(value)\n+\n     return structured\n", "issue": "Classifiers: Python version sort order\n<!--\r\n    NOTE: This issue should be for problems with PyPI itself, including:\r\n    * pypi.org\r\n    * test.pypi.org\r\n    * files.pythonhosted.org\r\n\r\n    This issue should NOT be for a project installed from PyPI. If you are\r\n    having an issue with a specific package, you should reach out to the\r\n    maintainers of that project directly instead.\r\n\r\n    Furthermore, this issue should NOT be for any non-PyPI properties (like\r\n    python.org, docs.python.org, etc.)\r\n-->\r\n\r\n**Describe the bug**\r\n<!-- A clear and concise description the bug -->\r\n\r\nThe classifiers \"Programming Language :: Python :: 3.X\" aren't sorted in the right order on https://pypi.org as well as on https://test.pypi.org\r\n\r\nI'm defining the classifiers like this in the `setup.py` file.\r\n\r\n```\r\nclassifiers=[\r\n    \"Programming Language :: Python :: 3\",\r\n    \"Programming Language :: Python :: 3.6\",\r\n    \"Programming Language :: Python :: 3.7\",\r\n    \"Programming Language :: Python :: 3.8\",\r\n    \"Programming Language :: Python :: 3.9\",\r\n    \"Programming Language :: Python :: 3.10\"\r\n    ]\r\n```\r\n\r\nIn the navigation bar on pypy.org it will then appear like this: \r\n![image](https://user-images.githubusercontent.com/70264417/99465712-42797f00-293b-11eb-8f1a-dced842f433f.png)\r\n\r\nWith Python 3.10 at the top instead of at the bottom (after Python 3.9).\r\n\r\n\r\nTo give the visitors of pypi.org a better and faster overview over a project, it would be great if the Python classifiers were sorted by the Python versions.\r\n\r\n\r\n**Expected behavior**\r\n<!-- A clear and concise description of what you expected to happen -->\r\n\r\nClassifiers sorted by Python versions.\r\n\r\nPython :: 3\r\nPython :: 3.6\r\nPython :: 3.7\r\nPython :: 3.8\r\nPython :: 3.9\r\nPython :: 3.10\r\nPython :: 3.11\r\nPython :: 3.12\r\netc.\r\n\r\n**To Reproduce**\r\n<!-- Steps to reproduce the bug, or a link to PyPI where the bug is visible -->\r\n\r\nIt can be seen for example here: https://pypi.org/project/officeextractor/\r\n\n", "before_files": [{"content": "# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nimport binascii\nimport collections\nimport datetime\nimport enum\nimport hmac\nimport json\nimport re\nimport urllib.parse\n\nfrom email.utils import getaddresses\n\nimport html5lib\nimport html5lib.serializer\nimport html5lib.treewalkers\nimport jinja2\nimport packaging.version\nimport pytz\n\nfrom pyramid.threadlocal import get_current_request\n\nfrom warehouse.utils.http import is_valid_uri\n\n\nclass PackageType(enum.Enum):\n    bdist_dmg = \"OSX Disk Image\"\n    bdist_dumb = \"Dumb Binary\"\n    bdist_egg = \"Egg\"\n    bdist_msi = \"Windows MSI Installer\"\n    bdist_rpm = \"RPM\"\n    bdist_wheel = \"Wheel\"\n    bdist_wininst = \"Windows Installer\"\n    sdist = \"Source\"\n\n\ndef format_package_type(value):\n    try:\n        return PackageType[value].value\n    except KeyError:\n        return value\n\n\ndef _camo_url(request, url):\n    camo_url = request.registry.settings[\"camo.url\"].format(request=request)\n    camo_key = request.registry.settings[\"camo.key\"].encode(\"utf8\")\n    url = url.encode(\"utf8\")\n\n    path = \"/\".join(\n        [\n            hmac.new(camo_key, url, digestmod=\"sha1\").hexdigest(),\n            binascii.hexlify(url).decode(\"utf8\"),\n        ]\n    )\n\n    return urllib.parse.urljoin(camo_url, path)\n\n\n@jinja2.pass_context\ndef camoify(ctx, value):\n    request = ctx.get(\"request\") or get_current_request()\n\n    # Parse the rendered output and replace any inline images that don't point\n    # to HTTPS with camouflaged images.\n    tree_builder = html5lib.treebuilders.getTreeBuilder(\"dom\")\n    parser = html5lib.html5parser.HTMLParser(tree=tree_builder)\n    dom = parser.parse(value)\n\n    for element in dom.getElementsByTagName(\"img\"):\n        src = element.getAttribute(\"src\")\n        if src:\n            element.setAttribute(\"src\", request.camo_url(src))\n\n    tree_walker = html5lib.treewalkers.getTreeWalker(\"dom\")\n    html_serializer = html5lib.serializer.HTMLSerializer()\n    camoed = \"\".join(html_serializer.serialize(tree_walker(dom)))\n\n    return camoed\n\n\n_SI_SYMBOLS = [\"k\", \"M\", \"G\", \"T\", \"P\", \"E\", \"Z\", \"Y\"]\n\n\ndef shorten_number(value):\n    for i, symbol in enumerate(_SI_SYMBOLS):\n        magnitude = value / (1000 ** (i + 1))\n        if magnitude >= 1 and magnitude < 1000:\n            return \"{:.3g}{}\".format(magnitude, symbol)\n\n    return str(value)\n\n\ndef tojson(value):\n    return json.dumps(value, sort_keys=True, separators=(\",\", \":\"))\n\n\ndef urlparse(value):\n    return urllib.parse.urlparse(value)\n\n\ndef format_tags(tags):\n    # split tags\n    if re.search(r\",\", tags):\n        split_tags = re.split(r\"\\s*,\\s*\", tags)\n    elif re.search(r\";\", tags):\n        split_tags = re.split(r\"\\s*;\\s*\", tags)\n    else:\n        split_tags = re.split(r\"\\s+\", tags)\n\n    # strip whitespace, quotes, double quotes\n    stripped_tags = [re.sub(r'^[\"\\'\\s]+|[\"\\'\\s]+$', \"\", t) for t in split_tags]\n\n    # remove any empty tags\n    formatted_tags = [t for t in stripped_tags if t]\n\n    return formatted_tags\n\n\ndef format_classifiers(classifiers):\n    structured = collections.OrderedDict()\n\n    # Split up our classifiers into our data structure\n    for classifier in classifiers:\n        key, *value = classifier.split(\" :: \", 1)\n        if value:\n            if key not in structured:\n                structured[key] = []\n            structured[key].append(value[0])\n\n    return structured\n\n\ndef classifier_id(classifier):\n    return classifier.replace(\" \", \"_\").replace(\"::\", \".\")\n\n\ndef contains_valid_uris(items):\n    \"\"\"Returns boolean representing whether the input list contains any valid\n    URIs\n    \"\"\"\n    return any(is_valid_uri(i) for i in items)\n\n\ndef parse_version(version_str):\n    return packaging.version.parse(version_str)\n\n\ndef localize_datetime(timestamp):\n    return pytz.utc.localize(timestamp)\n\n\ndef ctime(timestamp):\n    return datetime.datetime.fromtimestamp(timestamp)\n\n\ndef is_recent(timestamp):\n    if timestamp:\n        return timestamp + datetime.timedelta(days=30) > datetime.datetime.now()\n    return False\n\n\ndef format_author_email(metadata_email: str) -> tuple[str, str]:\n    \"\"\"\n    Return the name and email address from a metadata RFC-822 string.\n    Use Jinja's `first` and `last` to access each part in a template.\n    TODO: Support more than one email address, per RFC-822.\n    \"\"\"\n    author_emails = []\n    for author_name, author_email in getaddresses([metadata_email]):\n        if \"@\" not in author_email:\n            return author_name, \"\"\n        author_emails.append((author_name, author_email))\n    return author_emails[0][0], author_emails[0][1]\n\n\ndef includeme(config):\n    config.add_request_method(_camo_url, name=\"camo_url\")\n", "path": "warehouse/filters.py"}], "after_files": [{"content": "# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nimport binascii\nimport collections\nimport datetime\nimport enum\nimport hmac\nimport json\nimport re\nimport urllib.parse\n\nfrom email.utils import getaddresses\n\nimport html5lib\nimport html5lib.serializer\nimport html5lib.treewalkers\nimport jinja2\nimport packaging.version\nimport pytz\n\nfrom natsort import natsorted\nfrom pyramid.threadlocal import get_current_request\n\nfrom warehouse.utils.http import is_valid_uri\n\n\nclass PackageType(enum.Enum):\n    bdist_dmg = \"OSX Disk Image\"\n    bdist_dumb = \"Dumb Binary\"\n    bdist_egg = \"Egg\"\n    bdist_msi = \"Windows MSI Installer\"\n    bdist_rpm = \"RPM\"\n    bdist_wheel = \"Wheel\"\n    bdist_wininst = \"Windows Installer\"\n    sdist = \"Source\"\n\n\ndef format_package_type(value):\n    try:\n        return PackageType[value].value\n    except KeyError:\n        return value\n\n\ndef _camo_url(request, url):\n    camo_url = request.registry.settings[\"camo.url\"].format(request=request)\n    camo_key = request.registry.settings[\"camo.key\"].encode(\"utf8\")\n    url = url.encode(\"utf8\")\n\n    path = \"/\".join(\n        [\n            hmac.new(camo_key, url, digestmod=\"sha1\").hexdigest(),\n            binascii.hexlify(url).decode(\"utf8\"),\n        ]\n    )\n\n    return urllib.parse.urljoin(camo_url, path)\n\n\n@jinja2.pass_context\ndef camoify(ctx, value):\n    request = ctx.get(\"request\") or get_current_request()\n\n    # Parse the rendered output and replace any inline images that don't point\n    # to HTTPS with camouflaged images.\n    tree_builder = html5lib.treebuilders.getTreeBuilder(\"dom\")\n    parser = html5lib.html5parser.HTMLParser(tree=tree_builder)\n    dom = parser.parse(value)\n\n    for element in dom.getElementsByTagName(\"img\"):\n        src = element.getAttribute(\"src\")\n        if src:\n            element.setAttribute(\"src\", request.camo_url(src))\n\n    tree_walker = html5lib.treewalkers.getTreeWalker(\"dom\")\n    html_serializer = html5lib.serializer.HTMLSerializer()\n    camoed = \"\".join(html_serializer.serialize(tree_walker(dom)))\n\n    return camoed\n\n\n_SI_SYMBOLS = [\"k\", \"M\", \"G\", \"T\", \"P\", \"E\", \"Z\", \"Y\"]\n\n\ndef shorten_number(value):\n    for i, symbol in enumerate(_SI_SYMBOLS):\n        magnitude = value / (1000 ** (i + 1))\n        if magnitude >= 1 and magnitude < 1000:\n            return \"{:.3g}{}\".format(magnitude, symbol)\n\n    return str(value)\n\n\ndef tojson(value):\n    return json.dumps(value, sort_keys=True, separators=(\",\", \":\"))\n\n\ndef urlparse(value):\n    return urllib.parse.urlparse(value)\n\n\ndef format_tags(tags):\n    # split tags\n    if re.search(r\",\", tags):\n        split_tags = re.split(r\"\\s*,\\s*\", tags)\n    elif re.search(r\";\", tags):\n        split_tags = re.split(r\"\\s*;\\s*\", tags)\n    else:\n        split_tags = re.split(r\"\\s+\", tags)\n\n    # strip whitespace, quotes, double quotes\n    stripped_tags = [re.sub(r'^[\"\\'\\s]+|[\"\\'\\s]+$', \"\", t) for t in split_tags]\n\n    # remove any empty tags\n    formatted_tags = [t for t in stripped_tags if t]\n\n    return formatted_tags\n\n\ndef format_classifiers(classifiers):\n    structured = collections.OrderedDict()\n\n    # Split up our classifiers into our data structure\n    for classifier in classifiers:\n        key, *value = classifier.split(\" :: \", 1)\n        if value:\n            if key not in structured:\n                structured[key] = []\n            structured[key].append(value[0])\n\n    # Sort all the values in our data structure\n    for key, value in structured.items():\n        structured[key] = natsorted(value)\n\n    return structured\n\n\ndef classifier_id(classifier):\n    return classifier.replace(\" \", \"_\").replace(\"::\", \".\")\n\n\ndef contains_valid_uris(items):\n    \"\"\"Returns boolean representing whether the input list contains any valid\n    URIs\n    \"\"\"\n    return any(is_valid_uri(i) for i in items)\n\n\ndef parse_version(version_str):\n    return packaging.version.parse(version_str)\n\n\ndef localize_datetime(timestamp):\n    return pytz.utc.localize(timestamp)\n\n\ndef ctime(timestamp):\n    return datetime.datetime.fromtimestamp(timestamp)\n\n\ndef is_recent(timestamp):\n    if timestamp:\n        return timestamp + datetime.timedelta(days=30) > datetime.datetime.now()\n    return False\n\n\ndef format_author_email(metadata_email: str) -> tuple[str, str]:\n    \"\"\"\n    Return the name and email address from a metadata RFC-822 string.\n    Use Jinja's `first` and `last` to access each part in a template.\n    TODO: Support more than one email address, per RFC-822.\n    \"\"\"\n    author_emails = []\n    for author_name, author_email in getaddresses([metadata_email]):\n        if \"@\" not in author_email:\n            return author_name, \"\"\n        author_emails.append((author_name, author_email))\n    return author_emails[0][0], author_emails[0][1]\n\n\ndef includeme(config):\n    config.add_request_method(_camo_url, name=\"camo_url\")\n", "path": "warehouse/filters.py"}]}