Merge pull request #1424 from alex/verify-interfaces

Fixes #1024 -- a utility function for checking an implementor against an ABC
diff --git a/cryptography/utils.py b/cryptography/utils.py
index 1deb3d1..03c8c0e 100644
--- a/cryptography/utils.py
+++ b/cryptography/utils.py
@@ -13,6 +13,8 @@
 
 from __future__ import absolute_import, division, print_function
 
+import abc
+import inspect
 import sys
 
 
@@ -21,6 +23,7 @@
 
 def register_interface(iface):
     def register_decorator(klass):
+        verify_interface(iface, klass)
         iface.register(klass)
         return klass
     return register_decorator
@@ -30,6 +33,30 @@
     return property(lambda self: getattr(self, name))
 
 
+class InterfaceNotImplemented(Exception):
+    pass
+
+
+def verify_interface(iface, klass):
+    for method in iface.__abstractmethods__:
+        if not hasattr(klass, method):
+            raise InterfaceNotImplemented(
+                "{0} is missing a {1!r} method".format(klass, method)
+            )
+        if isinstance(getattr(iface, method), abc.abstractproperty):
+            # Can't properly verify these yet.
+            continue
+        spec = inspect.getargspec(getattr(iface, method))
+        actual = inspect.getargspec(getattr(klass, method))
+        if spec != actual:
+            raise InterfaceNotImplemented(
+                "{0}.{1}'s signature differs from the expected. Expected: "
+                "{2!r}. Received: {3!r}".format(
+                    klass, method, spec, actual
+                )
+            )
+
+
 def bit_length(x):
     if sys.version_info >= (2, 7):
         return x.bit_length()
diff --git a/tests/test_interfaces.py b/tests/test_interfaces.py
new file mode 100644
index 0000000..b988abe
--- /dev/null
+++ b/tests/test_interfaces.py
@@ -0,0 +1,63 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import abc
+
+import pytest
+
+import six
+
+from cryptography.utils import InterfaceNotImplemented, verify_interface
+
+
+class TestVerifyInterface(object):
+    def test_verify_missing_method(self):
+        @six.add_metaclass(abc.ABCMeta)
+        class SimpleInterface(object):
+            @abc.abstractmethod
+            def method(self):
+                """A simple method"""
+
+        class NonImplementer(object):
+            pass
+
+        with pytest.raises(InterfaceNotImplemented):
+            verify_interface(SimpleInterface, NonImplementer)
+
+    def test_different_arguments(self):
+        @six.add_metaclass(abc.ABCMeta)
+        class SimpleInterface(object):
+            @abc.abstractmethod
+            def method(self, a):
+                """Method with one argument"""
+
+        class NonImplementer(object):
+            def method(self):
+                """Method with no arguments"""
+
+        with pytest.raises(InterfaceNotImplemented):
+            verify_interface(SimpleInterface, NonImplementer)
+
+    def test_handles_abstract_property(self):
+        @six.add_metaclass(abc.ABCMeta)
+        class SimpleInterface(object):
+            @abc.abstractproperty
+            def property(self):
+                """An abstract property"""
+
+        class NonImplementer(object):
+            @property
+            def property(self):
+                """A concrete property"""
+
+        verify_interface(SimpleInterface, NonImplementer)