Repository: pypa/twine

Issue:
"twine upload" usually fails to upload .asc files
On the most recent Foolscap release, I signed the sdist tarballs as usual, and tried to use twine to upload everything:

```
% python setup.py sdist --formats=zip,gztar bdist_wheel
% ls dist
foolscap-0.9.1-py2-none-any.whl		foolscap-0.9.1.tar.gz			foolscap-0.9.1.zip
% (gpg sign them all)
% ls dist
foolscap-0.9.1-py2-none-any.whl		foolscap-0.9.1.tar.gz			foolscap-0.9.1.zip
foolscap-0.9.1-py2-none-any.whl.asc	foolscap-0.9.1.tar.gz.asc		foolscap-0.9.1.zip.asc
% python setup.py register
% twine upload dist/*
```

Twine uploaded the tar/zip/whl files, but ignored the .asc signatures, and the resulting [pypi page](https://pypi.python.org/pypi/foolscap/0.9.1) doesn't show them either.

After some digging, I found that `twine/upload.py upload()` will only use pre-signed .asc files if the command was run like `cd dist; twine upload *`. It won't use them if it was run as `cd dist; twine upload ./*` or `twine upload dist/*`. The problem seems to be that the `signatures` dictionary is indexed by the basename of the signature files, while the lookup key is using the full (original) filename of the tarball/etc with ".asc" appended.

I think it might be simpler and safer to have the code just check for a neighboring .asc file inside the upload loop, something like:

```python
for filename in uploads:
    package = PackageFile.from_filename(filename, comment)
    maybe_sig = package.signed_filename + ".asc"
    if os.path.exists(maybe_sig):
        package.gpg_signature = (os.path.basename(maybe_sig), sigdata)
    ...
```

I'll write up a patch for this. I started to look for a way of adding a test, but the code that looks for signatures happens deep enough in `upload()` that it'd need a oversized mock "Repository" class to exercise the .asc check without actually uploading anything. I'm not sure what the best way to approach the test would be.


Discussion:

Patch:
diff --git a/twine/commands/upload.py b/twine/commands/upload.py
index 032cc21..2bd4a52 100644
--- a/twine/commands/upload.py
+++ b/twine/commands/upload.py
@@ -67,11 +67,13 @@ def upload(dists, repository, sign, identity, username, password, comment,
     if not sign and identity:
         raise ValueError("sign must be given along with identity")
 
+    dists = find_dists(dists)
+
     # Determine if the user has passed in pre-signed distributions
     signatures = dict(
         (os.path.basename(d), d) for d in dists if d.endswith(".asc")
     )
-    dists = [i for i in dists if not i.endswith(".asc")]
+    uploads = [i for i in dists if not i.endswith(".asc")]
 
     config = utils.get_repository_from_config(config_file, repository)
 
@@ -86,24 +88,14 @@ def upload(dists, repository, sign, identity, username, password, comment,
 
     repository = Repository(config["repository"], username, password)
 
-    uploads = find_dists(dists)
-
     for filename in uploads:
         package = PackageFile.from_filename(filename, comment)
-        # Sign the dist if requested
-        # if sign:
-        #     sign_file(sign_with, filename, identity)
 
-        # signed_name = os.path.basename(filename) + ".asc"
-        signed_name = package.signed_filename
+        signed_name = package.signed_basefilename
         if signed_name in signatures:
-            with open(signatures[signed_name], "rb") as gpg:
-                package.gpg_signature = (signed_name, gpg.read())
-                # data["gpg_signature"] = (signed_name, gpg.read())
+            package.add_gpg_signature(signatures[signed_name], signed_name)
         elif sign:
             package.sign(sign_with, identity)
-            # with open(filename + ".asc", "rb") as gpg:
-            #     data["gpg_signature"] = (signed_name, gpg.read())
 
         resp = repository.upload(package)
 
diff --git a/twine/package.py b/twine/package.py
index e80116a..e062c71 100644
--- a/twine/package.py
+++ b/twine/package.py
@@ -49,6 +49,7 @@ class PackageFile(object):
         self.filetype = filetype
         self.safe_name = pkg_resources.safe_name(metadata.name)
         self.signed_filename = self.filename + '.asc'
+        self.signed_basefilename = self.basefilename + '.asc'
         self.gpg_signature = None
 
         md5_hash = hashlib.md5()
@@ -141,6 +142,13 @@ class PackageFile(object):
 
         return data
 
+    def add_gpg_signature(self, signature_filepath, signature_filename):
+        if self.gpg_signature is not None:
+            raise ValueError('GPG Signature can only be added once')
+
+        with open(signature_filepath, "rb") as gpg:
+            self.gpg_signature = (signature_filename, gpg.read())
+
     def sign(self, sign_with, identity):
         print("Signing {0}".format(self.basefilename))
         gpg_args = (sign_with, "--detach-sign")
@@ -149,5 +157,4 @@ class PackageFile(object):
         gpg_args += ("-a", self.filename)
         subprocess.check_call(gpg_args)
 
-        with open(self.signed_filename, "rb") as gpg:
-            self.pg_signature = (self.signed_filename, gpg.read())
+        self.add_gpg_signature(self.signed_filename, self.signed_basefilename)


Test patch:
diff --git a/tests/test_package.py b/tests/test_package.py
index fcc827a..d28eec1 100644
--- a/tests/test_package.py
+++ b/tests/test_package.py
@@ -55,3 +55,18 @@ def test_sign_file_with_identity(monkeypatch):
         pass
     args = ('gpg', '--detach-sign', '--local-user', 'identity', '-a', filename)
     assert replaced_check_call.calls == [pretend.call(args)]
+
+
+def test_package_signed_name_is_correct():
+    filename = 'tests/fixtures/deprecated-pypirc'
+
+    pkg = package.PackageFile(
+        filename=filename,
+        comment=None,
+        metadata=pretend.stub(name="deprecated-pypirc"),
+        python_version=None,
+        filetype=None
+    )
+
+    assert pkg.signed_basefilename == "deprecated-pypirc.asc"
+    assert pkg.signed_filename == (filename + '.asc')
diff --git a/tests/test_upload.py b/tests/test_upload.py
index b40660f..7f99510 100644
--- a/tests/test_upload.py
+++ b/tests/test_upload.py
@@ -66,6 +66,7 @@ def test_find_dists_handles_real_files():
 
 def test_get_config_old_format(tmpdir):
     pypirc = os.path.join(str(tmpdir), ".pypirc")
+    dists = ["tests/fixtures/twine-1.5.0-py2.py3-none-any.whl"]
 
     with open(pypirc, "w") as fp:
         fp.write(textwrap.dedent("""
@@ -75,7 +76,7 @@ def test_get_config_old_format(tmpdir):
         """))
 
     try:
-        upload.upload(dists="foo", repository="pypi", sign=None, identity=None,
+        upload.upload(dists=dists, repository="pypi", sign=None, identity=None,
                       username=None, password=None, comment=None,
                       sign_with=None, config_file=pypirc, skip_existing=False)
     except KeyError as err: