Repository: pypi/warehouse

Issue:
Version lookup should take PEP 440 normalization into account
Looking up `1.0.0.0.0` should return `1.0` if it exists, etc.



Discussion:
We probably don't need this at launch, legacy PyPI doesn't do this (and it'll be easier to do once legacy PyPI is gone since we won't have to implement it twice).

Dustin was just working on version normalization in pypa/packaging#121 so this will be shipped soon.

Resolving patch:
diff --git a/warehouse/forklift/legacy.py b/warehouse/forklift/legacy.py
--- a/warehouse/forklift/legacy.py
+++ b/warehouse/forklift/legacy.py
@@ -835,79 +835,67 @@ def file_upload(request):
         )
 
     try:
+        canonical_version = packaging.utils.canonicalize_version(
+            form.version.data
+        )
         release = (
             request.db.query(Release)
-                      .filter(
-                            (Release.project == project) &
-                            (Release.version == form.version.data)).one()
+            .filter(
+                (Release.project == project) &
+                (Release.canonical_version == canonical_version)
+            )
+            .one()
         )
     except NoResultFound:
-        # We didn't find a release with the exact version string, try and see
-        # if one exists with an equivalent parsed version
-        releases = (
-            request.db.query(Release)
-            .filter(Release.project == project)
-            .all()
-        )
-        versions = {
-            packaging.version.parse(release.version): release
-            for release in releases
-        }
-
-        try:
-            release = versions[packaging.version.parse(form.version.data)]
-        except KeyError:
-            release = Release(
-                project=project,
-                _classifiers=[
-                    c for c in all_classifiers
-                    if c.classifier in form.classifiers.data
-                ],
-                _pypi_hidden=False,
-                dependencies=list(_construct_dependencies(
-                    form,
-                    {
-                        "requires": DependencyKind.requires,
-                        "provides": DependencyKind.provides,
-                        "obsoletes": DependencyKind.obsoletes,
-                        "requires_dist": DependencyKind.requires_dist,
-                        "provides_dist": DependencyKind.provides_dist,
-                        "obsoletes_dist": DependencyKind.obsoletes_dist,
-                        "requires_external": DependencyKind.requires_external,
-                        "project_urls": DependencyKind.project_url,
-                    }
-                )),
-                canonical_version=packaging.utils.canonicalize_version(
-                    form.version.data
-                ),
-                **{
-                    k: getattr(form, k).data
-                    for k in {
-                        # This is a list of all the fields in the form that we
-                        # should pull off and insert into our new release.
-                        "version",
-                        "summary", "description", "license",
-                        "author", "author_email",
-                        "maintainer", "maintainer_email",
-                        "keywords", "platform",
-                        "home_page", "download_url",
-                        "requires_python",
-                    }
+        release = Release(
+            project=project,
+            _classifiers=[
+                c for c in all_classifiers
+                if c.classifier in form.classifiers.data
+            ],
+            _pypi_hidden=False,
+            dependencies=list(_construct_dependencies(
+                form,
+                {
+                    "requires": DependencyKind.requires,
+                    "provides": DependencyKind.provides,
+                    "obsoletes": DependencyKind.obsoletes,
+                    "requires_dist": DependencyKind.requires_dist,
+                    "provides_dist": DependencyKind.provides_dist,
+                    "obsoletes_dist": DependencyKind.obsoletes_dist,
+                    "requires_external": DependencyKind.requires_external,
+                    "project_urls": DependencyKind.project_url,
                 }
-            )
-            request.db.add(release)
-            # TODO: This should be handled by some sort of database trigger or
-            #       a SQLAlchemy hook or the like instead of doing it inline in
-            #       this view.
-            request.db.add(
-                JournalEntry(
-                    name=release.project.name,
-                    version=release.version,
-                    action="new release",
-                    submitted_by=request.user,
-                    submitted_from=request.remote_addr,
-                ),
-            )
+            )),
+            canonical_version=canonical_version,
+            **{
+                k: getattr(form, k).data
+                for k in {
+                    # This is a list of all the fields in the form that we
+                    # should pull off and insert into our new release.
+                    "version",
+                    "summary", "description", "license",
+                    "author", "author_email",
+                    "maintainer", "maintainer_email",
+                    "keywords", "platform",
+                    "home_page", "download_url",
+                    "requires_python",
+                }
+            }
+        )
+        request.db.add(release)
+        # TODO: This should be handled by some sort of database trigger or
+        #       a SQLAlchemy hook or the like instead of doing it inline in
+        #       this view.
+        request.db.add(
+            JournalEntry(
+                name=release.project.name,
+                version=release.version,
+                action="new release",
+                submitted_by=request.user,
+                submitted_from=request.remote_addr,
+            ),
+        )
 
     # TODO: We need a better solution to this than to just do it inline inside
     #       this method. Ideally the version field would just be sortable, but
diff --git a/warehouse/packaging/models.py b/warehouse/packaging/models.py
--- a/warehouse/packaging/models.py
+++ b/warehouse/packaging/models.py
@@ -14,6 +14,7 @@
 
 from collections import OrderedDict
 
+import packaging.utils
 from citext import CIText
 from pyramid.security import Allow
 from pyramid.threadlocal import get_current_request
@@ -133,13 +134,16 @@ class Project(SitemapMixin, db.ModelBase):
 
     def __getitem__(self, version):
         session = orm.object_session(self)
+        canonical_version = packaging.utils.canonicalize_version(version)
 
         try:
             return (
                 session.query(Release)
-                       .filter((Release.project == self) &
-                               (Release.version == version))
-                       .one()
+                .filter(
+                    (Release.project == self) &
+                    (Release.canonical_version == canonical_version)
+                )
+                .one()
             )
         except NoResultFound:
             raise KeyError from None


Test patch:
diff --git a/tests/common/db/packaging.py b/tests/common/db/packaging.py
--- a/tests/common/db/packaging.py
+++ b/tests/common/db/packaging.py
@@ -15,6 +15,7 @@
 
 import factory
 import factory.fuzzy
+import packaging.utils
 
 from warehouse.packaging.models import (
     BlacklistedProject, Dependency, DependencyKind, File, JournalEntry,
@@ -39,7 +40,9 @@ class Meta:
     name = factory.LazyAttribute(lambda o: o.project.name)
     project = factory.SubFactory(ProjectFactory)
     version = factory.Sequence(lambda n: str(n) + ".0")
-    canonical_version = factory.Sequence(lambda n: str(n) + ".0")
+    canonical_version = factory.LazyAttribute(
+        lambda o: packaging.utils.canonicalize_version(o.version)
+    )
     _pypi_ordering = factory.Sequence(lambda n: n)
 
     uploader = factory.SubFactory(UserFactory)
diff --git a/tests/unit/packaging/test_models.py b/tests/unit/packaging/test_models.py
--- a/tests/unit/packaging/test_models.py
+++ b/tests/unit/packaging/test_models.py
@@ -73,6 +73,12 @@ def test_traversal_finds(self, db_request):
 
         assert project[release.version] == release
 
+    def test_traversal_finds_canonical_version(self, db_request):
+        project = DBProjectFactory.create()
+        release = DBReleaseFactory.create(version='1.0', project=project)
+
+        assert project['1.0.0'] == release
+
     def test_traversal_cant_find(self, db_request):
         project = DBProjectFactory.create()