Repository: pypa/twine

Issue:
FileNotFoundError: [Errno 2] No such file or directory: 'gpg': 'gpg'
On platform where `gpg` executable does not exist twine failsto sign with an errorl like:
```
FileNotFoundError: [Errno 2] No such file or directory: 'gpg': 'gpg'
```

This happens even if `gpg2` executable is present and working, use of gpg2 is quite common and twine should be able to prefer instead of the very old gpg.

Adding an alias does not solve the problem

Discussion:

Patch:
diff --git a/twine/exceptions.py b/twine/exceptions.py
index 6ced6dd..47a12ee 100644
--- a/twine/exceptions.py
+++ b/twine/exceptions.py
@@ -85,6 +85,12 @@ class InvalidSigningConfiguration(TwineException):
     pass
 
 
+class InvalidSigningExecutable(TwineException):
+    """Signing executable must be installed on system."""
+
+    pass
+
+
 class InvalidConfiguration(TwineException):
     """Raised when configuration is invalid."""
 
diff --git a/twine/package.py b/twine/package.py
index 9fe89d2..4212aae 100644
--- a/twine/package.py
+++ b/twine/package.py
@@ -33,6 +33,12 @@ from twine.wheel import Wheel
 from twine.wininst import WinInst
 from twine import exceptions
 
+try:
+    FileNotFoundError = FileNotFoundError
+except NameError:
+    FileNotFoundError = IOError  # Py2
+
+
 DIST_TYPES = {
     "bdist_wheel": Wheel,
     "bdist_wininst": WinInst,
@@ -174,10 +180,31 @@ class PackageFile(object):
         if identity:
             gpg_args += ("--local-user", identity)
         gpg_args += ("-a", self.filename)
-        subprocess.check_call(gpg_args)
+        self.run_gpg(gpg_args)
 
         self.add_gpg_signature(self.signed_filename, self.signed_basefilename)
 
+    @classmethod
+    def run_gpg(cls, gpg_args):
+        try:
+            subprocess.check_call(gpg_args)
+            return
+        except FileNotFoundError:
+            if gpg_args[0] != "gpg":
+                raise exceptions.InvalidSigningExecutable(
+                    "{} executable not available.".format(gpg_args[0]))
+
+        print("gpg executable not available. Attempting fallback to gpg2.")
+        try:
+            subprocess.check_call(("gpg2",) + gpg_args[1:])
+        except FileNotFoundError:
+            print("gpg2 executable not available.")
+            raise exceptions.InvalidSigningExecutable(
+                "'gpg' or 'gpg2' executables not available. "
+                "Try installing one of these or specifying an executable "
+                "with the --sign-with flag."
+            )
+
 
 Hexdigest = collections.namedtuple('Hexdigest', ['md5', 'sha2', 'blake2'])
 


Test patch:
diff --git a/tests/test_package.py b/tests/test_package.py
index ecb301a..8e4fbbb 100644
--- a/tests/test_package.py
+++ b/tests/test_package.py
@@ -59,6 +59,48 @@ def test_sign_file_with_identity(monkeypatch):
     assert replaced_check_call.calls == [pretend.call(args)]
 
 
+def test_run_gpg_raises_exception_if_no_gpgs(monkeypatch):
+    replaced_check_call = pretend.raiser(
+        package.FileNotFoundError('not found')
+    )
+    monkeypatch.setattr(package.subprocess, 'check_call', replaced_check_call)
+    gpg_args = ('gpg', '--detach-sign', '-a', 'pypircfile')
+
+    with pytest.raises(exceptions.InvalidSigningExecutable) as err:
+        package.PackageFile.run_gpg(gpg_args)
+
+    assert 'executables not available' in err.value.args[0]
+
+
+def test_run_gpg_raises_exception_if_not_using_gpg(monkeypatch):
+    replaced_check_call = pretend.raiser(
+        package.FileNotFoundError('not found')
+    )
+    monkeypatch.setattr(package.subprocess, 'check_call', replaced_check_call)
+    gpg_args = ('not_gpg', '--detach-sign', '-a', 'pypircfile')
+
+    with pytest.raises(exceptions.InvalidSigningExecutable) as err:
+        package.PackageFile.run_gpg(gpg_args)
+
+    assert 'not_gpg executable not available' in err.value.args[0]
+
+
+def test_run_gpg_falls_back_to_gpg2(monkeypatch):
+
+    def check_call(arg_list):
+        if arg_list[0] == 'gpg':
+            raise package.FileNotFoundError('gpg not found')
+
+    replaced_check_call = pretend.call_recorder(check_call)
+    monkeypatch.setattr(package.subprocess, 'check_call', replaced_check_call)
+    gpg_args = ('gpg', '--detach-sign', '-a', 'pypircfile')
+
+    package.PackageFile.run_gpg(gpg_args)
+
+    gpg2_args = replaced_check_call.calls[1].args
+    assert gpg2_args[0][0] == 'gpg2'
+
+
 def test_package_signed_name_is_correct():
     filename = 'tests/fixtures/deprecated-pypirc'