Repository: pypi/warehouse

Issue:
Improve sorting on simple page
I'd like to submit a patch for this but I have a few questions :)

First I'll describe what I'd like to do...

## sort by version number

See https://pypi.org/simple/pre-commit/

You'll notice that `0.10.0` erroneously sorts *before* `0.2.0` (I'd like to fix this)

## investigation

I've found the code which does this sorting [here](https://github.com/pypa/warehouse/blob/3bdfe5a89cc9a922ee97304c98384c24822a09ee/warehouse/legacy/api/simple.py#L76-L89)

This seems to just sort by filename, but by inspecting and viewing [this page](https://pypi.org/simple/pre-commit-mirror-maker/) I notice it seems to ignore `_` vs. `-` (which is good, that's what I want to continue to happen but I'm just not seeing it from the code!)

## other questions

The `File` objects which come back from the database contain a `.version` attribute that I'd like to use to participate in sorting, my main question is: **Can I depend on this version to be a valid [PEP440](https://www.python.org/dev/peps/pep-0440/) version and use something like `pkg_resources.parse_version`?**

I'd basically like to replicate something close to the sorting which @chriskuehl's [dumb-pypi](https://github.com/chriskuehl/dumb-pypi) does [here](https://github.com/chriskuehl/dumb-pypi/blob/fd0f93fc2e82cbd9bae41b3c60c5f006b2319c60/dumb_pypi/main.py#L77-L91).

Thanks in advance :)

---

**Good First Issue**: This issue is good for first time contributors. If there is not a corresponding pull request for this issue, it is up for grabs. For directions for getting set up, see our [Getting Started Guide](https://warehouse.pypa.io/development/getting-started/). If you are working on this issue and have questions, please feel free to ask them here, [`#pypa-dev` on Freenode](https://webchat.freenode.net/?channels=%23pypa-dev), or the [pypa-dev mailing list](https://groups.google.com/forum/#!forum/pypa-dev).


Discussion:
It does just sort by filename, largely because the order doesn't *really* matter, as long as it's stable between page loads. That being said, I wouldn't turn down a PR that does this. The version attributes are not guaranteed to be PEP 440 compatible (some of them are old enough that they predate PEP 440). Ideally you're use ``packaging.version.parse_version`` which will handle that case fine.
@dstufft if it sorts by filename, I would expect the following sorting on this page: https://pypi.org/simple/pre-commit-mirror-maker/

```python
[
    'pre-commit-mirror-maker-0.1.0.tar.gz',
    'pre-commit-mirror-maker-0.1.1.tar.gz',
    'pre-commit-mirror-maker-0.1.2.tar.gz',
    'pre-commit-mirror-maker-0.2.0.tar.gz',
    'pre-commit-mirror-maker-0.3.0.tar.gz',
    'pre-commit-mirror-maker-0.3.1.tar.gz',
    'pre-commit-mirror-maker-0.3.2.tar.gz',
    'pre-commit-mirror-maker-0.4.0.tar.gz',
    'pre_commit_mirror_maker-0.3.2-py2-none-any.whl',
    'pre_commit_mirror_maker-0.4.0-py2-none-any.whl',
    'pre_commit_mirror_maker-0.4.0-py3-none-any.whl',
]
```

but the wheel files are properly sorted alongside their source distributions -- does postgres normalize this?
I don't honestly recall the specifics of how it sorts other than it was by filename.
Awesome, thanks for the prompt replies :) I'll do some more digging!

Resolving patch:
diff --git a/warehouse/legacy/api/simple.py b/warehouse/legacy/api/simple.py
--- a/warehouse/legacy/api/simple.py
+++ b/warehouse/legacy/api/simple.py
@@ -10,6 +10,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+
+from packaging.version import parse
 from pyramid.httpexceptions import HTTPMovedPermanently
 from pyramid.view import view_config
 from sqlalchemy import func
@@ -73,7 +75,7 @@ def simple_detail(project, request):
     request.response.headers["X-PyPI-Last-Serial"] = str(project.last_serial)
 
     # Get all of the files for this project.
-    files = (
+    files = sorted(
         request.db.query(File)
         .options(joinedload(File.release))
         .filter(
@@ -84,8 +86,8 @@ def simple_detail(project, request):
                           .with_entities(Release.version)
             )
         )
-        .order_by(File.filename)
-        .all()
+        .all(),
+        key=lambda f: (parse(f.version), f.packagetype)
     )
 
     return {"project": project, "files": files}


Test patch:
diff --git a/tests/unit/legacy/api/test_simple.py b/tests/unit/legacy/api/test_simple.py
--- a/tests/unit/legacy/api/test_simple.py
+++ b/tests/unit/legacy/api/test_simple.py
@@ -161,3 +161,64 @@ def test_with_files_with_serial(self, db_request):
             "files": files,
         }
         assert db_request.response.headers["X-PyPI-Last-Serial"] == str(je.id)
+
+    def test_with_files_with_version_multi_digit(self, db_request):
+        project = ProjectFactory.create()
+        releases = [
+            ReleaseFactory.create(project=project)
+            for _ in range(3)
+        ]
+        release_versions = [
+            "0.3.0rc1", "0.3.0", "0.3.0-post0", "0.14.0", "4.2.0", "24.2.0"
+        ]
+
+        for release, version in zip(releases, release_versions):
+            release.version = version
+
+        tar_files = [
+            FileFactory.create(
+                release=r,
+                filename="{}-{}.tar.gz".format(project.name, r.version),
+                packagetype="sdist"
+            )
+            for r in releases
+        ]
+        wheel_files = [
+            FileFactory.create(
+                release=r,
+                filename="{}-{}.whl".format(project.name, r.version),
+                packagetype="bdist_wheel"
+            )
+            for r in releases
+        ]
+        egg_files = [
+            FileFactory.create(
+                release=r,
+                filename="{}-{}.egg".format(project.name, r.version),
+                packagetype="bdist_egg"
+            )
+            for r in releases
+        ]
+
+        files = []
+        for files_release in \
+                zip(egg_files, wheel_files, tar_files):
+            files += files_release
+
+        db_request.matchdict["name"] = project.normalized_name
+        user = UserFactory.create()
+        je = JournalEntryFactory.create(name=project.name, submitted_by=user)
+
+        # Make sure that we get any changes made since the JournalEntry was
+        # saved.
+        db_request.db.refresh(project)
+        import pprint
+        pprint.pprint(simple.simple_detail(project, db_request)['files'])
+        pprint.pprint(files)
+
+        assert simple.simple_detail(project, db_request) == {
+            "project": project,
+            "files": files,
+        }
+
+        assert db_request.response.headers["X-PyPI-Last-Serial"] == str(je.id)