Repository: pypi/warehouse

Issue:
Search default or option: exclude projects with zero downloadable distributions
**What's the problem this feature will solve?**
Some PyPI projects have zero past releases, or have released distributions in the past but then deleted the distributions. When users search for projects, they are probably searching for packages they can download, and may be frustrated if the results include projects without any downloadable files.

**Describe the solution you'd like**
Either by default, or as a faceted search option, in search results, we could filter out packages that don't have downloadable distributions available.

**Additional context**
Requested in https://github.com/pypa/warehouse/issues/194#issuecomment-41197783 by @agronholm and seconded in the subsequent comment by @dstufft.


Discussion:
This can be done by adjusting the query used for reindexing to exclude releases without files: https://github.com/pypa/warehouse/blob/dc120b9c0f8fba9dc98809e68e1b56a1d1bda5e6/warehouse/search/tasks.py#L33-L95

Resolving patch:
diff --git a/warehouse/search/tasks.py b/warehouse/search/tasks.py
--- a/warehouse/search/tasks.py
+++ b/warehouse/search/tasks.py
@@ -41,7 +41,7 @@ def _project_docs(db, project_name=None):
 
     releases_list = (
         db.query(Release.id)
-        .filter(Release.yanked.is_(False))
+        .filter(Release.yanked.is_(False), Release.files)
         .order_by(
             Release.project_id,
             Release.is_prerelease.nullslast(),


Test patch:
diff --git a/tests/unit/search/test_tasks.py b/tests/unit/search/test_tasks.py
--- a/tests/unit/search/test_tasks.py
+++ b/tests/unit/search/test_tasks.py
@@ -31,7 +31,7 @@
     unindex_project,
 )
 
-from ...common.db.packaging import ProjectFactory, ReleaseFactory
+from ...common.db.packaging import FileFactory, ProjectFactory, ReleaseFactory
 
 
 def test_project_docs(db_session):
@@ -45,6 +45,16 @@ def test_project_docs(db_session):
         for p in projects
     }
 
+    for p in projects:
+        for r in releases[p]:
+            r.files = [
+                FileFactory.create(
+                    release=r,
+                    filename="{}-{}.tar.gz".format(p.name, r.version),
+                    python_version="source",
+                )
+            ]
+
     assert list(_project_docs(db_session)) == [
         {
             "_id": p.normalized_name,
@@ -75,6 +85,16 @@ def test_single_project_doc(db_session):
         for p in projects
     }
 
+    for p in projects:
+        for r in releases[p]:
+            r.files = [
+                FileFactory.create(
+                    release=r,
+                    filename="{}-{}.tar.gz".format(p.name, r.version),
+                    python_version="source",
+                )
+            ]
+
     assert list(_project_docs(db_session, project_name=projects[1].name)) == [
         {
             "_id": p.normalized_name,
@@ -95,6 +115,47 @@ def test_single_project_doc(db_session):
     ]
 
 
+def test_project_docs_empty(db_session):
+    projects = [ProjectFactory.create() for _ in range(2)]
+    releases = {
+        p: sorted(
+            [ReleaseFactory.create(project=p) for _ in range(3)],
+            key=lambda r: packaging.version.parse(r.version),
+            reverse=True,
+        )
+        for p in projects
+    }
+
+    project_with_files = projects[0]
+    for r in releases[project_with_files]:
+        r.files = [
+            FileFactory.create(
+                release=r,
+                filename="{}-{}.tar.gz".format(project_with_files.name, r.version),
+                python_version="source",
+            )
+        ]
+
+    assert list(_project_docs(db_session)) == [
+        {
+            "_id": p.normalized_name,
+            "_type": "doc",
+            "_source": {
+                "created": p.created,
+                "name": p.name,
+                "normalized_name": p.normalized_name,
+                "version": [r.version for r in prs],
+                "latest_version": first(prs, key=lambda r: not r.is_prerelease).version,
+                "description": first(
+                    prs, key=lambda r: not r.is_prerelease
+                ).description.raw,
+            },
+        }
+        for p, prs in sorted(releases.items(), key=lambda x: x[0].id)
+        if p == project_with_files
+    ]
+
+
 class FakeESIndices:
     def __init__(self):
         self.indices = {}