Commits

Andriy Kornatskyy committed ca5b902

Introduced looks-like checks for duck typing conformance.

Comments (0)

Files changed (2)

src/wheezy/core/introspection.py

 """ ``introspection`` module.
 """
 
+import warnings
+
+from inspect import getargspec
+from inspect import isclass
+from inspect import isfunction
+
 from wheezy.core.comp import __import__
+from wheezy.core.descriptors import attribute
 
 
 def import_name(fullname):
     namespace, name = fullname.rsplit('.', 1)
     obj = __import__(namespace, None, None, [name])
     return getattr(obj, name)
+
+
+class looks(object):
+    """ Performs duck typing checks for two classes.
+
+        Typical use::
+
+            assert looks(IFoo, ignore_argspec=['pex']).like(Foo)
+    """
+
+    def __init__(self, cls, ignore_funcs=None, ignore_argspec=None):
+        """
+            *cls* - a class to be checked
+            *ignore_funcs* - a list of functions to ignore
+            *ignore_argspec* - a list of functions to ignore arguments spec.
+        """
+        self.declarations = declarations(cls)
+        self.ignore_funcs = ignore_funcs or []
+        self.ignore_argspec = ignore_argspec or []
+
+    def like(self, cls):
+        """ Check if `self.cls` can be used as duck typing for `cls`.
+
+            *cls* - class to be checked for duck typing.
+        """
+        for n, t in declarations(cls).items():
+            if n in self.ignore_funcs:
+                continue
+            if n not in self.declarations:
+                warn("'%s': is missing." % n)
+                return False
+            else:
+                t2 = self.declarations[n]
+                if isfunction(t) and isfunction(t2):
+                    if n in self.ignore_argspec:
+                        continue
+                    if getargspec(t) != getargspec(t2):
+                        warn("'%s': argument names or defaults "
+                             "have no match." % n)
+                        return False
+                elif t2.__class__ is not t.__class__:
+                    warn("'%s': is not %s." % (n, t.__class__.__name__))
+                    return False
+        return True
+
+
+# region: internal details
+
+def declarations(cls):
+    return dict((n, v) for n, v in cls.__dict__.items()
+                if not n.startswith('_'))
+
+
+def warn(message):
+    warnings.warn(message, stacklevel=3)

src/wheezy/core/tests/test_introspection.py

+
+""" Unit tests for ``wheezy.core.introspection``.
+"""
+
+import unittest
+
+try:
+    from warnings import catch_warnings
+except ImportError:
+    pass
+else:
+
+    class LooksLikeTestCase(unittest.TestCase):
+
+        def setUp(self):
+            self.ctx = catch_warnings(record=True)
+            self.w = self.ctx.__enter__()
+
+        def tearDown(self):
+            self.ctx.__exit__(None, None, None)
+
+        def assert_warning(self, msg):
+            assert len(self.w) == 1
+            self.assertEquals(msg, str(self.w[-1].message))
+
+        def test_func(self):
+            """ Tests if there is any function missing.
+            """
+            from wheezy.core.introspection import looks
+
+            class IFoo(object):
+                def foo(self):
+                    pass
+
+            class Foo(object):
+                def bar(self):
+                    pass
+
+            assert not looks(Foo).like(IFoo)
+            self.assert_warning("'foo': is missing.")
+
+        def test_ignore_func(self):
+            """ Tests if function is ignored.
+            """
+            from wheezy.core.introspection import looks
+
+            class IFoo(object):
+                def foo(self):
+                    pass
+
+            class Foo(object):
+                def bar(self):
+                    pass
+
+            assert looks(Foo, ignore_funcs='foo').like(IFoo)
+
+        def test_args(self):
+            """ Tests if there any function args corresponds.
+            """
+            from wheezy.core.introspection import looks
+
+            class IFoo(object):
+                def foo(self, a, b=1):
+                    pass
+
+            class Foo(object):
+                def foo(self, a, b):
+                    pass
+
+            assert not looks(Foo).like(IFoo)
+            self.assert_warning("'foo': argument names or defaults "
+                                "have no match.")
+
+        def test_ignore_args(self):
+            """ Tests if function args ignored.
+            """
+            from wheezy.core.introspection import looks
+
+            class IFoo(object):
+                def foo(self, a, b=1):
+                    pass
+
+            class Foo(object):
+                def foo(self, a, b):
+                    pass
+
+            assert looks(Foo, ignore_argspec='foo').like(IFoo)
+
+        def test_decorator(self):
+            """ Tests if there any method decorators corresponds.
+            """
+            from wheezy.core.introspection import looks
+
+            class IFoo(object):
+                @property
+                def foo(self):
+                    pass
+
+            class Foo(object):
+                def foo(self):
+                    pass
+
+            assert not looks(Foo).like(IFoo)
+            self.assert_warning("'foo': is not property.")
+
+        def test_various(self):
+            """ Tests if there are no errors.
+            """
+            from wheezy.core.introspection import looks
+
+            class IFoo(object):
+
+                def foo(self, a, b=1):
+                    pass
+
+                @property
+                def bar(self):
+                    pass
+
+            class Foo(object):
+                def foo(self, a, b=1):
+                    pass
+
+                @property
+                def bar(self):
+                    pass
+
+            assert looks(Foo).like(IFoo)
+            assert len(self.w) == 0