Skip to content

Commit 8592b3e

Browse files
author
Scott Sanderson
committed
ENH: Add Interface.from_class.
Adds a new classmethod, `from_class` to `Interface`, for generating an interface from an existing class. This is useful when you want to create a class whose interface matches the interface of an existing class you can't control (e.g., for creating a test double of class defined in a library).
1 parent da10b6b commit 8592b3e

File tree

2 files changed

+187
-0
lines changed

2 files changed

+187
-0
lines changed

interface/interface.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ class InvalidImplementation(TypeError):
3535

3636
is_interface_field_name = complement(CLASS_ATTRIBUTE_WHITELIST.__contains__)
3737

38+
TRIVIAL_CLASS_ATTRIBUTES = frozenset(dir(type('_', (object,), {})))
39+
3840

3941
def static_get_type_attr(t, name):
4042
"""
@@ -267,6 +269,38 @@ class Interface(with_metaclass(InterfaceMeta)):
267269
def __new__(cls, *args, **kwargs):
268270
raise TypeError("Can't instantiate interface %s" % getname(cls))
269271

272+
@classmethod
273+
def from_class(cls, existing_class, subset=None, name=None):
274+
"""Create an interface from an existing class.
275+
276+
Parameters
277+
----------
278+
existing_class : type
279+
The type from which to extract an interface.
280+
subset : list[str], optional
281+
List of methods that should be included in the interface.
282+
Default is to use all attributes not defined in an empty class.
283+
name : str, optional
284+
Name of the generated interface.
285+
Default is ``existing_class.__name__ + 'Interface'``.
286+
287+
Returns
288+
-------
289+
interface : type
290+
A new interface class with stubs generated from ``existing_class``.
291+
"""
292+
if name is None:
293+
name = existing_class.__name__ + 'Interface'
294+
295+
if subset is None:
296+
subset = set(dir(existing_class)) - TRIVIAL_CLASS_ATTRIBUTES
297+
298+
return InterfaceMeta(
299+
name,
300+
(Interface,),
301+
{name: getattr(existing_class, name) for name in subset},
302+
)
303+
270304

271305
empty_set = frozenset([])
272306

interface/tests/test_interface.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -720,3 +720,156 @@ class C failed to implement interface I:
720720
)
721721

722722
assert actual_message == expected_message
723+
724+
725+
@pytest.mark.parametrize('name', ['MyInterface', None])
726+
def test_interface_from_class(name):
727+
728+
class MyClass(object): # pragma: nocover
729+
def method1(self, x):
730+
raise AssertionError("method1 called")
731+
732+
def method2(self, y):
733+
raise AssertionError("method2 called")
734+
735+
iface = Interface.from_class(MyClass, name=name)
736+
737+
if name is None:
738+
expected_name = 'MyClassInterface'
739+
else:
740+
expected_name = name
741+
742+
assert iface.__name__ == expected_name
743+
744+
with pytest.raises(InvalidImplementation) as e:
745+
class C(implements(iface)): # pragma: nocover
746+
pass
747+
748+
actual_message = str(e.value)
749+
expected_message = dedent(
750+
"""
751+
class C failed to implement interface {iface}:
752+
753+
The following methods of {iface} were not implemented:
754+
- method1(self, x)
755+
- method2(self, y)"""
756+
).format(iface=expected_name)
757+
758+
assert actual_message == expected_message
759+
760+
761+
def test_interface_from_class_method_subset():
762+
763+
class C(object): # pragma: nocover
764+
765+
def method1(self, x):
766+
pass
767+
768+
def method2(self, y):
769+
pass
770+
771+
iface = Interface.from_class(C, subset=['method1'])
772+
773+
class Impl(implements(iface)): # pragma: nocover
774+
def method1(self, x):
775+
pass
776+
777+
with pytest.raises(InvalidImplementation) as e:
778+
779+
class BadImpl(implements(iface)): # pragma: nocover
780+
def method2(self, y):
781+
pass
782+
783+
actual_message = str(e.value)
784+
expected_message = dedent(
785+
"""
786+
class BadImpl failed to implement interface CInterface:
787+
788+
The following methods of CInterface were not implemented:
789+
- method1(self, x)"""
790+
)
791+
792+
assert actual_message == expected_message
793+
794+
795+
def test_interface_from_class_inherited_methods():
796+
797+
class Base(object): # pragma: nocover
798+
def base_method(self, x):
799+
pass
800+
801+
class Derived(Base): # pragma: nocover
802+
def derived_method(self, y):
803+
pass
804+
805+
iface = Interface.from_class(Derived)
806+
807+
# Should be fine
808+
class Impl(implements(iface)): # pragma: nocover
809+
def base_method(self, x):
810+
pass
811+
812+
def derived_method(self, y):
813+
pass
814+
815+
with pytest.raises(InvalidImplementation) as e:
816+
817+
class BadImpl(implements(iface)): # pragma: nocover
818+
def derived_method(self, y):
819+
pass
820+
821+
actual_message = str(e.value)
822+
expected_message = dedent(
823+
"""
824+
class BadImpl failed to implement interface DerivedInterface:
825+
826+
The following methods of DerivedInterface were not implemented:
827+
- base_method(self, x)"""
828+
)
829+
assert actual_message == expected_message
830+
831+
with pytest.raises(InvalidImplementation) as e:
832+
833+
class BadImpl(implements(iface)): # pragma: nocover
834+
def base_method(self, x):
835+
pass
836+
837+
actual_message = str(e.value)
838+
expected_message = dedent(
839+
"""
840+
class BadImpl failed to implement interface DerivedInterface:
841+
842+
The following methods of DerivedInterface were not implemented:
843+
- derived_method(self, y)"""
844+
)
845+
846+
assert actual_message == expected_message
847+
848+
849+
def test_interface_from_class_magic_methods():
850+
851+
class HasMagicMethods(object): # pragma: nocover
852+
def __getitem__(self, key):
853+
return key
854+
855+
iface = Interface.from_class(HasMagicMethods)
856+
857+
# Should be fine
858+
class Impl(implements(iface)): # pragma: nocover
859+
def __getitem__(self, key):
860+
return key
861+
862+
with pytest.raises(InvalidImplementation) as e:
863+
864+
class BadImpl(implements(iface)): # pragma: nocover
865+
pass
866+
867+
actual_message = str(e.value)
868+
expected_message = dedent(
869+
"""
870+
class BadImpl failed to implement interface HasMagicMethodsInterface:
871+
872+
The following methods of HasMagicMethodsInterface were not implemented:
873+
- __getitem__(self, key)"""
874+
)
875+
assert actual_message == expected_message

0 commit comments

Comments
 (0)