Repository: pypi/warehouse

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/



Discussion:
Thanks for filing an issue. PyPI is technically sorting these "correctly", since the original classifier order is not preserved in the project metadata, classifiers are just strings, and the classifiers are being sorted lexicographically. This can be seen in a number of other places as well, like https://pypi.org/classifiers/.

I agree that it does feel unintuitive for humans, though. I'd be open to accepting a change that takes any integer/float value of segment of a classifier into account when sorting. However, this should probably be computed once and pre-built into https://github.com/pypa/trove-classifiers instead of being done on the fly everywhere by PyPI. For instances like this, where we'd have to do it on the fly, `trove-classifiers` should also provide a sorting function as well.

I've filed https://github.com/pypa/trove-classifiers/issues/56 to capture that and marked this issue as blocked until it's implemented.
This is also an issue in the list of filters in the sidebar of the search results page, e.g. https://pypi.org/search/?q=beautifulsoup

![Screenshot from 2021-01-19 23-43-05](https://user-images.githubusercontent.com/5687998/105128436-08e70080-5ab1-11eb-9f12-05cf9390b9f5.png)

Also an issue in https://pypi.org/pypi?%3Aaction=list_classifiers
I assume this has been fixed now by https://github.com/pypa/trove-classifiers/pull/57 👍🏼
Indeed!
We still need to pull in the new version of `trove-classifiers` here and update our usage of it to use the `sorted_classifiers` list.
Is this actually fixed? Looking at https://pypi.org/project/officeextractor/ the ordering is still wrong.
Looks like we need to update this function which does some secondary sorting which may no longer be necessary:

https://github.com/pypa/warehouse/blob/7fc3ce5bd7ecc93ef54c1652787fb5e7757fe6f2/warehouse/filters.py#L126-L144

which is used here:

https://github.com/pypa/warehouse/blob/20c947a4cd4a9667bf0f31722e77b87964ae195c/warehouse/templates/includes/packaging/project-data.html#L128

Re-opening...
Please see PR https://github.com/pypa/warehouse/pull/10006.
This is now fixed, from https://pypi.org/project/officeextractor/:

![image](https://user-images.githubusercontent.com/294415/132566140-ba662bbd-ee79-48ac-84c9-3017ba317ae8.png)

There may still be some pages that have the incorrect ordering in cache, but these will slowly be updated as these pages fall out of cache.

This has broken, here's the earlier fixed https://pypi.org/project/officeextractor/:

<img width="203" alt="image" src="https://user-images.githubusercontent.com/1324225/184849356-865c5e11-c1db-4ed0-8670-11caebe55477.png">

I'm also seeing the "bad" order at https://test.pypi.org/project/sphinxcontrib-towncrier/0.3.0a1.dev46/:
```
 Programming Language

    Python :: 3.10
    Python :: 3.11
    Python :: 3.6
    Python :: 3.7
    Python :: 3.8
    Python :: 3.9
```

Resolving patch:
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 @@ def format_classifiers(classifiers):
                 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
 
 


Test patch:
diff --git a/tests/unit/test_filters.py b/tests/unit/test_filters.py
--- a/tests/unit/test_filters.py
+++ b/tests/unit/test_filters.py
@@ -146,6 +146,19 @@ def test_format_tags(inp, expected):
             ["Foo :: Bar :: Baz", "Vleep :: Foo", "Foo :: Bar :: Qux"],
             [("Foo", ["Bar :: Baz", "Bar :: Qux"]), ("Vleep", ["Foo"])],
         ),
+        (
+            [
+                "Programming Language :: Python :: 3.11",
+                "Programming Language :: Python :: 3.10",
+                "Programming Language :: Python :: 3.8",
+            ],
+            [
+                (
+                    "Programming Language",
+                    ["Python :: 3.8", "Python :: 3.10", "Python :: 3.11"],
+                )
+            ],
+        ),
     ],
 )
 def test_format_classifiers(inp, expected):