Procházet zdrojové kódy

Add available() class method to all signature vendors

Jelmer Vernooij před 3 týdny
rodič
revize
4539a7b16c
2 změnil soubory, kde provedl 89 přidání a 0 odebrání
  1. 64 0
      dulwich/signature.py
  2. 25 0
      tests/test_signature.py

+ 64 - 0
dulwich/signature.py

@@ -43,6 +43,15 @@ class SignatureVendor:
         """
         self.config = config
 
+    @classmethod
+    def available(cls) -> bool:
+        """Check if this signature vendor is available.
+
+        Returns:
+          True if the vendor's dependencies are available, False otherwise
+        """
+        return True  # Base class is always available
+
     def sign(self, data: bytes, keyid: str | None = None) -> bytes:
         """Sign data with a key.
 
@@ -100,6 +109,17 @@ class GPGSignatureVendor(SignatureVendor):
             except KeyError:
                 pass
 
+    @classmethod
+    def available(cls) -> bool:
+        """Check if the gpg Python package is available.
+
+        Returns:
+          True if gpg package can be imported, False otherwise
+        """
+        import importlib.util
+
+        return importlib.util.find_spec("gpg") is not None
+
     def sign(self, data: bytes, keyid: str | None = None) -> bytes:
         """Sign data with a GPG key.
 
@@ -213,6 +233,17 @@ class GPGCliSignatureVendor(SignatureVendor):
         else:
             self.gpg_command = "gpg"
 
+    @classmethod
+    def available(cls) -> bool:
+        """Check if the gpg command is available.
+
+        Returns:
+          True if gpg command is in PATH, False otherwise
+        """
+        import shutil
+
+        return shutil.which("gpg") is not None
+
     def sign(self, data: bytes, keyid: str | None = None) -> bytes:
         """Sign data with a GPG key using the command-line tool.
 
@@ -360,6 +391,17 @@ class X509SignatureVendor(SignatureVendor):
         else:
             self.gpgsm_command = "gpgsm"
 
+    @classmethod
+    def available(cls) -> bool:
+        """Check if the gpgsm command is available.
+
+        Returns:
+          True if gpgsm command is in PATH, False otherwise
+        """
+        import shutil
+
+        return shutil.which("gpgsm") is not None
+
     def sign(self, data: bytes, keyid: str | None = None) -> bytes:
         """Sign data with an X.509 certificate using gpgsm.
 
@@ -515,6 +557,17 @@ class SSHSigSignatureVendor(SignatureVendor):
             except KeyError:
                 pass
 
+    @classmethod
+    def available(cls) -> bool:
+        """Check if the sshsig Python package is available.
+
+        Returns:
+          True if sshsig package can be imported, False otherwise
+        """
+        import importlib.util
+
+        return importlib.util.find_spec("sshsig") is not None
+
     def sign(self, data: bytes, keyid: str | None = None) -> bytes:
         """Sign data with an SSH key.
 
@@ -677,6 +730,17 @@ class SSHCliSignatureVendor(SignatureVendor):
             except KeyError:
                 pass
 
+    @classmethod
+    def available(cls) -> bool:
+        """Check if the ssh-keygen command is available.
+
+        Returns:
+          True if ssh-keygen command is in PATH, False otherwise
+        """
+        import shutil
+
+        return shutil.which("ssh-keygen") is not None
+
     def sign(self, data: bytes, keyid: str | None = None) -> bytes:
         """Sign data with an SSH key using ssh-keygen.
 

+ 25 - 0
tests/test_signature.py

@@ -80,6 +80,11 @@ class GPGSignatureVendorTests(unittest.TestCase):
         vendor = GPGSignatureVendor()
         self.assertIsNone(vendor.min_trust_level)
 
+    def test_available(self) -> None:
+        """Test that available() returns boolean."""
+        result = GPGSignatureVendor.available()
+        self.assertIsInstance(result, bool)
+
     def test_sign_and_verify(self) -> None:
         """Test basic sign and verify cycle.
 
@@ -282,6 +287,11 @@ class GPGCliSignatureVendorTests(unittest.TestCase):
         vendor = GPGCliSignatureVendor(config=config)
         self.assertEqual(vendor.gpg_command, "gpg")
 
+    def test_available(self) -> None:
+        """Test that available() returns boolean."""
+        result = GPGCliSignatureVendor.available()
+        self.assertIsInstance(result, bool)
+
 
 class X509SignatureVendorTests(unittest.TestCase):
     """Tests for X509SignatureVendor."""
@@ -313,6 +323,11 @@ class X509SignatureVendorTests(unittest.TestCase):
         vendor = X509SignatureVendor(config=config)
         self.assertEqual(vendor.gpgsm_command, "gpgsm")
 
+    def test_available(self) -> None:
+        """Test that available() returns boolean."""
+        result = X509SignatureVendor.available()
+        self.assertIsInstance(result, bool)
+
     @unittest.skipIf(
         shutil.which("gpgsm") is None, "gpgsm command not available in PATH"
     )
@@ -430,6 +445,11 @@ class SSHSigSignatureVendorTests(unittest.TestCase):
         self.assertEqual(vendor.allowed_signers_file, "/path/to/allowed")
         self.assertEqual(vendor.default_key_command, "ssh-add -L")
 
+    def test_available(self) -> None:
+        """Test that available() returns boolean."""
+        result = SSHSigSignatureVendor.available()
+        self.assertIsInstance(result, bool)
+
     def test_verify_with_cli_generated_signature(self) -> None:
         """Test verifying a signature created by SSH CLI vendor."""
         import os
@@ -640,6 +660,11 @@ class SSHCliSignatureVendorTests(unittest.TestCase):
         vendor = SSHCliSignatureVendor(config=config)
         self.assertEqual(vendor.revocation_file, "/path/to/revoked_keys")
 
+    def test_available(self) -> None:
+        """Test that available() returns boolean."""
+        result = SSHCliSignatureVendor.available()
+        self.assertIsInstance(result, bool)
+
 
 class DetectSignatureFormatTests(unittest.TestCase):
     """Tests for detect_signature_format function."""