Repository: pypi/warehouse

Issue:
gpg_signature: Should not be a tuple
Sometime in the last two weeks, gpg-signed uploads to https://upload.pypi.org/legacy/ began to break with the following error message:
```
Uploading distributions to https://upload.pypi.org/legacy/
Signing ${snip}.whl

You need a passphrase to unlock the secret key for
user: "${snip}"
2048-bit RSA key, ID ${snip}, created ${snip}

Uploading ${snip}.whl
HTTPError: 400 Client Error: gpg_signature: Should not be a tuple. for url: https://upload.pypi.org/legacy/
```

This seems potentially related to #3049 ?

This is in a venv using twine for the upload and the following dependency versions. 
```
$ twine-deps.venv/bin/twine --version
twine version 1.9.1 (pkginfo: 1.4.1, requests: 2.18.4, setuptools: 38.5.1,
requests-toolbelt: 0.8.0, tqdm: 4.19.5)

$ twine-deps.venv/bin/python --version
Python 2.7.14
```
Neither our invoke of `twine`, nor the dependencies have changed in the last two weeks.


Discussion:

Resolving patch:
diff --git a/warehouse/forklift/legacy.py b/warehouse/forklift/legacy.py
--- a/warehouse/forklift/legacy.py
+++ b/warehouse/forklift/legacy.py
@@ -676,10 +676,11 @@ def file_upload(request):
         raise _exc_with_message(HTTPBadRequest, "Unknown protocol version.")
 
     # Check if any fields were supplied as a tuple and have become a
-    # FieldStorage. The 'content' field _should_ be a FieldStorage, however.
+    # FieldStorage. The 'content' and 'gpg_signature' fields _should_ be a
+    # FieldStorage, however.
     # ref: https://github.com/pypa/warehouse/issues/2185
     # ref: https://github.com/pypa/warehouse/issues/2491
-    for field in set(request.POST) - {'content'}:
+    for field in set(request.POST) - {'content', 'gpg_signature'}:
         values = request.POST.getall(field)
         if any(isinstance(value, FieldStorage) for value in values):
             raise _exc_with_message(


Test patch:
diff --git a/tests/unit/forklift/test_legacy.py b/tests/unit/forklift/test_legacy.py
--- a/tests/unit/forklift/test_legacy.py
+++ b/tests/unit/forklift/test_legacy.py
@@ -1066,17 +1066,19 @@ def test_successful_upload(self, tmpdir, monkeypatch, pyramid_config,
 
         db_request.user = user
         db_request.remote_addr = "10.10.10.40"
+
+        content = FieldStorage()
+        content.filename = filename
+        content.file = io.BytesIO(b"A fake file.")
+        content.type = "application/tar"
+
         db_request.POST = MultiDict({
             "metadata_version": "1.2",
             "name": project.name,
             "version": release.version,
             "filetype": "sdist",
             "pyversion": "source",
-            "content": pretend.stub(
-                filename=filename,
-                file=io.BytesIO(b"A fake file."),
-                type="application/tar",
-            ),
+            "content": content,
         })
         db_request.POST.extend([
             ("classifiers", "Environment :: Other Environment"),
@@ -1084,13 +1086,14 @@ def test_successful_upload(self, tmpdir, monkeypatch, pyramid_config,
         db_request.POST.update(digests)
 
         if has_signature:
-            db_request.POST["gpg_signature"] = pretend.stub(
-                filename=filename + ".asc",
-                file=io.BytesIO(
-                    b"-----BEGIN PGP SIGNATURE-----\n"
-                    b" This is a Fake Signature"
-                ),
+            gpg_signature = FieldStorage()
+            gpg_signature.filename = filename + ".asc"
+            gpg_signature.file = io.BytesIO(
+                b"-----BEGIN PGP SIGNATURE-----\n"
+                b" This is a Fake Signature"
             )
+            db_request.POST["gpg_signature"] = gpg_signature
+            assert isinstance(db_request.POST["gpg_signature"], FieldStorage)
 
         @pretend.call_recorder
         def storage_service_store(path, file_path, *, meta):