[Python-checkins] r58099 - in python/trunk: Lib/abc.py Lib/test/test_abc.py Lib/test/test_typechecks.py Objects/abstract.c

guido.van.rossum python-checkins at python.org
Tue Sep 11 00:36:03 CEST 2007


Author: guido.van.rossum
Date: Tue Sep 11 00:36:02 2007
New Revision: 58099

Added:
   python/trunk/Lib/abc.py   (contents, props changed)
   python/trunk/Lib/test/test_abc.py   (contents, props changed)
   python/trunk/Lib/test/test_typechecks.py   (contents, props changed)
Modified:
   python/trunk/Objects/abstract.c
Log:
Patch # 1026 by Benjamin Aranguren (with Alex Martelli):
Backport abc.py and isinstance/issubclass overloading to 2.6.

I had to backport test_typechecks.py myself, and make one small change
to abc.py to avoid duplicate work when x.__class__ and type(x) are the
same.



Added: python/trunk/Lib/abc.py
==============================================================================
--- (empty file)
+++ python/trunk/Lib/abc.py	Tue Sep 11 00:36:02 2007
@@ -0,0 +1,206 @@
+# Copyright 2007 Google, Inc. All Rights Reserved.
+# Licensed to PSF under a Contributor Agreement.
+
+"""Abstract Base Classes (ABCs) according to PEP 3119."""
+
+
+def abstractmethod(funcobj):
+    """A decorator indicating abstract methods.
+
+    Requires that the metaclass is ABCMeta or derived from it.  A
+    class that has a metaclass derived from ABCMeta cannot be
+    instantiated unless all of its abstract methods are overridden.
+    The abstract methods can be called using any of the the normal
+    'super' call mechanisms.
+
+    Usage:
+
+        class C(metaclass=ABCMeta):
+            @abstractmethod
+            def my_abstract_method(self, ...):
+                ...
+    """
+    funcobj.__isabstractmethod__ = True
+    return funcobj
+
+
+class abstractproperty(property):
+    """A decorator indicating abstract properties.
+
+    Requires that the metaclass is ABCMeta or derived from it.  A
+    class that has a metaclass derived from ABCMeta cannot be
+    instantiated unless all of its abstract properties are overridden.
+    The abstract properties can be called using any of the the normal
+    'super' call mechanisms.
+
+    Usage:
+
+        class C(metaclass=ABCMeta):
+            @abstractproperty
+            def my_abstract_property(self):
+                ...
+
+    This defines a read-only property; you can also define a read-write
+    abstract property using the 'long' form of property declaration:
+
+        class C(metaclass=ABCMeta):
+            def getx(self): ...
+            def setx(self, value): ...
+            x = abstractproperty(getx, setx)
+    """
+    __isabstractmethod__ = True
+
+
+class _Abstract(object):
+
+    """Helper class inserted into the bases by ABCMeta (using _fix_bases()).
+
+    You should never need to explicitly subclass this class.
+
+    There should never be a base class between _Abstract and object.
+    """
+
+    def __new__(cls, *args, **kwds):
+        am = cls.__dict__.get("__abstractmethods__")
+        if am:
+            raise TypeError("Can't instantiate abstract class %s "
+                            "with abstract methods %s" %
+                            (cls.__name__, ", ".join(sorted(am))))
+        if (args or kwds) and cls.__init__ is object.__init__:
+            raise TypeError("Can't pass arguments to __new__ "
+                            "without overriding __init__")
+        return object.__new__(cls)
+
+    @classmethod
+    def __subclasshook__(cls, subclass):
+        """Abstract classes can override this to customize issubclass().
+
+        This is invoked early on by __subclasscheck__() below.  It
+        should return True, False or NotImplemented.  If it returns
+        NotImplemented, the normal algorithm is used.  Otherwise, it
+        overrides the normal algorithm (and the outcome is cached).
+        """
+        return NotImplemented
+
+
+def _fix_bases(bases):
+    """Helper method that inserts _Abstract in the bases if needed."""
+    for base in bases:
+        if issubclass(base, _Abstract):
+            # _Abstract is already a base (maybe indirectly)
+            return bases
+    if object in bases:
+        # Replace object with _Abstract
+        return tuple([_Abstract if base is object else base
+                      for base in bases])
+    # Append _Abstract to the end
+    return bases + (_Abstract,)
+
+
+class ABCMeta(type):
+
+    """Metaclass for defining Abstract Base Classes (ABCs).
+
+    Use this metaclass to create an ABC.  An ABC can be subclassed
+    directly, and then acts as a mix-in class.  You can also register
+    unrelated concrete classes (even built-in classes) and unrelated
+    ABCs as 'virtual subclasses' -- these and their descendants will
+    be considered subclasses of the registering ABC by the built-in
+    issubclass() function, but the registering ABC won't show up in
+    their MRO (Method Resolution Order) nor will method
+    implementations defined by the registering ABC be callable (not
+    even via super()).
+
+    """
+
+    # A global counter that is incremented each time a class is
+    # registered as a virtual subclass of anything.  It forces the
+    # negative cache to be cleared before its next use.
+    _abc_invalidation_counter = 0
+
+    def __new__(mcls, name, bases, namespace):
+        bases = _fix_bases(bases)
+        cls = super(ABCMeta, mcls).__new__(mcls, name, bases, namespace)
+        # Compute set of abstract method names
+        abstracts = set(name
+                     for name, value in namespace.items()
+                     if getattr(value, "__isabstractmethod__", False))
+        for base in bases:
+            for name in getattr(base, "__abstractmethods__", set()):
+                value = getattr(cls, name, None)
+                if getattr(value, "__isabstractmethod__", False):
+                    abstracts.add(name)
+        cls.__abstractmethods__ = abstracts
+        # Set up inheritance registry
+        cls._abc_registry = set()
+        cls._abc_cache = set()
+        cls._abc_negative_cache = set()
+        cls._abc_negative_cache_version = ABCMeta._abc_invalidation_counter
+        return cls
+
+    def register(cls, subclass):
+        """Register a virtual subclass of an ABC."""
+        if not isinstance(cls, type):
+            raise TypeError("Can only register classes")
+        if issubclass(subclass, cls):
+            return  # Already a subclass
+        # Subtle: test for cycles *after* testing for "already a subclass";
+        # this means we allow X.register(X) and interpret it as a no-op.
+        if issubclass(cls, subclass):
+            # This would create a cycle, which is bad for the algorithm below
+            raise RuntimeError("Refusing to create an inheritance cycle")
+        cls._abc_registry.add(subclass)
+        ABCMeta._abc_invalidation_counter += 1  # Invalidate negative cache
+
+    def _dump_registry(cls, file=None):
+        """Debug helper to print the ABC registry."""
+        print >> file, "Class: %s.%s" % (cls.__module__, cls.__name__)
+        print >> file, "Inv.counter: %s" % ABCMeta._abc_invalidation_counter
+        for name in sorted(cls.__dict__.keys()):
+            if name.startswith("_abc_"):
+                value = getattr(cls, name)
+                print >> file, "%s: %r" % (name, value)
+
+    def __instancecheck__(cls, instance):
+        """Override for isinstance(instance, cls)."""
+        return any(cls.__subclasscheck__(c)
+                   for c in set([instance.__class__, type(instance)]))
+
+    def __subclasscheck__(cls, subclass):
+        """Override for issubclass(subclass, cls)."""
+        # Check cache
+        if subclass in cls._abc_cache:
+            return True
+        # Check negative cache; may have to invalidate
+        if cls._abc_negative_cache_version < ABCMeta._abc_invalidation_counter:
+            # Invalidate the negative cache
+            cls._abc_negative_cache = set()
+            cls._abc_negative_cache_version = ABCMeta._abc_invalidation_counter
+        elif subclass in cls._abc_negative_cache:
+            return False
+        # Check the subclass hook
+        ok = cls.__subclasshook__(subclass)
+        if ok is not NotImplemented:
+            assert isinstance(ok, bool)
+            if ok:
+                cls._abc_cache.add(subclass)
+            else:
+                cls._abc_negative_cache.add(subclass)
+            return ok
+        # Check if it's a direct subclass
+        if cls in subclass.__mro__:
+            cls._abc_cache.add(subclass)
+            return True
+        # Check if it's a subclass of a registered class (recursive)
+        for rcls in cls._abc_registry:
+            if issubclass(subclass, rcls):
+                cls._abc_registry.add(subclass)
+                return True
+        # Check if it's a subclass of a subclass (recursive)
+        for scls in cls.__subclasses__():
+            if issubclass(subclass, scls):
+                cls._abc_registry.add(subclass)
+                return True
+        # No dice; update negative cache
+        cls._abc_negative_cache.add(subclass)
+        return False

Added: python/trunk/Lib/test/test_abc.py
==============================================================================
--- (empty file)
+++ python/trunk/Lib/test/test_abc.py	Tue Sep 11 00:36:02 2007
@@ -0,0 +1,142 @@
+# Copyright 2007 Google, Inc. All Rights Reserved.
+# Licensed to PSF under a Contributor Agreement.
+
+"""Unit tests for abc.py."""
+
+import sys
+import unittest
+from test import test_support
+
+import abc
+__metaclass__ = type
+
+
+class TestABC(unittest.TestCase):
+
+    def test_abstractmethod_basics(self):
+        @abc.abstractmethod
+        def foo(self): pass
+        self.assertEqual(foo.__isabstractmethod__, True)
+        def bar(self): pass
+        self.assertEqual(hasattr(bar, "__isabstractmethod__"), False)
+
+    def test_abstractproperty_basics(self):
+        @abc.abstractproperty
+        def foo(self): pass
+        self.assertEqual(foo.__isabstractmethod__, True)
+        def bar(self): pass
+        self.assertEqual(hasattr(bar, "__isabstractmethod__"), False)
+
+        class C:
+            __metaclass__ = abc.ABCMeta
+            @abc.abstractproperty
+            def foo(self): return 3
+        class D(C):
+            @property
+            def foo(self): return super(D, self).foo
+        self.assertEqual(D().foo, 3)
+
+    def test_abstractmethod_integration(self):
+        for abstractthing in [abc.abstractmethod, abc.abstractproperty]:
+            class C:
+                __metaclass__ = abc.ABCMeta
+                @abstractthing
+                def foo(self): pass  # abstract
+                def bar(self): pass  # concrete
+            self.assertEqual(C.__abstractmethods__, set(["foo"]))
+            self.assertRaises(TypeError, C)  # because foo is abstract
+            class D(C):
+                def bar(self): pass  # concrete override of concrete
+            self.assertEqual(D.__abstractmethods__, set(["foo"]))
+            self.assertRaises(TypeError, D)  # because foo is still abstract
+            class E(D):
+                def foo(self): pass
+            self.assertEqual(E.__abstractmethods__, set())
+            E()  # now foo is concrete, too
+            class F(E):
+                @abstractthing
+                def bar(self): pass  # abstract override of concrete
+            self.assertEqual(F.__abstractmethods__, set(["bar"]))
+            self.assertRaises(TypeError, F)  # because bar is abstract now
+
+    def test_registration_basics(self):
+        class A:
+            __metaclass__ = abc.ABCMeta
+        class B:
+            pass
+        b = B()
+        self.assertEqual(issubclass(B, A), False)
+        self.assertEqual(isinstance(b, A), False)
+        A.register(B)
+        self.assertEqual(issubclass(B, A), True)
+        self.assertEqual(isinstance(b, A), True)
+        class C(B):
+            pass
+        c = C()
+        self.assertEqual(issubclass(C, A), True)
+        self.assertEqual(isinstance(c, A), True)
+
+    def test_registration_builtins(self):
+        class A:
+            __metaclass__ = abc.ABCMeta
+        A.register(int)
+        self.assertEqual(isinstance(42, A), True)
+        self.assertEqual(issubclass(int, A), True)
+        class B(A):
+            pass
+        B.register(basestring)
+        self.assertEqual(isinstance("", A), True)
+        self.assertEqual(issubclass(str, A), True)
+
+    def test_registration_edge_cases(self):
+        class A:
+            __metaclass__ = abc.ABCMeta
+        A.register(A)  # should pass silently
+        class A1(A):
+            pass
+        self.assertRaises(RuntimeError, A1.register, A)  # cycles not allowed
+        class B:
+            pass
+        A1.register(B)  # ok
+        A1.register(B)  # should pass silently
+        class C(A):
+            pass
+        A.register(C)  # should pass silently
+        self.assertRaises(RuntimeError, C.register, A)  # cycles not allowed
+        C.register(B)  # ok
+
+    def test_registration_transitiveness(self):
+        class A:
+            __metaclass__ = abc.ABCMeta
+        self.failUnless(issubclass(A, A))
+        class B:
+            __metaclass__ = abc.ABCMeta
+        self.failIf(issubclass(A, B))
+        self.failIf(issubclass(B, A))
+        class C:
+            __metaclass__ = abc.ABCMeta
+        A.register(B)
+        class B1(B):
+            pass
+        self.failUnless(issubclass(B1, A))
+        class C1(C):
+            pass
+        B1.register(C1)
+        self.failIf(issubclass(C, B))
+        self.failIf(issubclass(C, B1))
+        self.failUnless(issubclass(C1, A))
+        self.failUnless(issubclass(C1, B))
+        self.failUnless(issubclass(C1, B1))
+        C1.register(int)
+        class MyInt(int):
+            pass
+        self.failUnless(issubclass(MyInt, A))
+        self.failUnless(isinstance(42, A))
+
+
+def test_main():
+    test_support.run_unittest(TestABC)
+
+
+if __name__ == "__main__":
+    unittest.main()

Added: python/trunk/Lib/test/test_typechecks.py
==============================================================================
--- (empty file)
+++ python/trunk/Lib/test/test_typechecks.py	Tue Sep 11 00:36:02 2007
@@ -0,0 +1,77 @@
+"""Unit tests for __instancecheck__ and __subclasscheck__."""
+
+import unittest
+from test import test_support
+
+
+class ABC(type):
+
+    def __instancecheck__(cls, inst):
+        """Implement isinstance(inst, cls)."""
+        return any(cls.__subclasscheck__(c)
+                   for c in set([type(inst), inst.__class__]))
+
+    def __subclasscheck__(cls, sub):
+        """Implement issubclass(sub, cls)."""
+        candidates = cls.__dict__.get("__subclass__", set()) | set([cls])
+        return any(c in candidates for c in sub.mro())
+
+
+class Integer:
+
+    __metaclass__ = ABC
+
+    __subclass__ = set([int])
+
+
+class SubInt(Integer):
+
+    pass
+
+
+class Evil:
+    def __instancecheck__(self, inst): return False
+
+
+class TypeChecksTest(unittest.TestCase):
+
+    def testIsSubclassInternal(self):
+        self.assertEqual(Integer.__subclasscheck__(int), True)
+        self.assertEqual(Integer.__subclasscheck__(float), False)
+
+    def testIsSubclassBuiltin(self):
+        self.assertEqual(issubclass(int, Integer), True)
+        self.assertEqual(issubclass(float, Integer), False)
+
+    def testIsInstanceBuiltin(self):
+        self.assertEqual(isinstance(42, Integer), True)
+        self.assertEqual(isinstance(3.14, Integer), False)
+
+    def testIsInstanceActual(self):
+        self.assertEqual(isinstance(Integer(), Integer), True)
+
+    def testIsSubclassActual(self):
+        self.assertEqual(issubclass(Integer, Integer), True)
+
+    def testSubclassBehavior(self):
+        self.assertEqual(issubclass(SubInt, Integer), True)
+        self.assertEqual(issubclass(SubInt, SubInt), True)
+        self.assertEqual(issubclass(Integer, SubInt), False)
+        self.assertEqual(issubclass(int, SubInt), False)
+        self.assertEqual(isinstance(SubInt(), Integer), True)
+        self.assertEqual(isinstance(SubInt(), SubInt), True)
+        self.assertEqual(isinstance(42, SubInt), False)
+
+    def testInfiniteRecursionCaughtProperly(self):
+        e = Evil()
+        # This invokes isinstance() recursively, until the stack is exhausted.
+        self.assertRaises(RuntimeError, isinstance, e, Evil)
+        # XXX How to check the same situation for issubclass()?
+
+
+def test_main():
+    test_support.run_unittest(TypeChecksTest)
+
+
+if __name__ == "__main__":
+    unittest.main()

Modified: python/trunk/Objects/abstract.c
==============================================================================
--- python/trunk/Objects/abstract.c	(original)
+++ python/trunk/Objects/abstract.c	Tue Sep 11 00:36:02 2007
@@ -2279,6 +2279,27 @@
 int
 PyObject_IsInstance(PyObject *inst, PyObject *cls)
 {
+	PyObject *t, *v, *tb;
+	PyObject *checker;
+	PyErr_Fetch(&t, &v, &tb);
+	checker = PyObject_GetAttrString(cls, "__instancecheck__");
+	PyErr_Restore(t, v, tb);
+	if (checker != NULL) {
+		PyObject *res;
+		int ok = -1;
+		if (Py_EnterRecursiveCall(" in __instancecheck__")) {
+			Py_DECREF(checker);
+			return ok;
+		}
+		res = PyObject_CallFunctionObjArgs(checker, inst, NULL);
+		Py_LeaveRecursiveCall();
+		Py_DECREF(checker);
+		if (res != NULL) {
+			ok = PyObject_IsTrue(res);
+			Py_DECREF(res);
+		}
+		return ok;
+	}
     return recursive_isinstance(inst, cls, Py_GetRecursionLimit());
 }
 
@@ -2334,6 +2355,25 @@
 int
 PyObject_IsSubclass(PyObject *derived, PyObject *cls)
 {
+	PyObject *t, *v, *tb;
+	PyObject *checker;
+	PyErr_Fetch(&t, &v, &tb);
+	checker = PyObject_GetAttrString(cls, "__subclasscheck__");
+	PyErr_Restore(t, v, tb);
+	if (checker != NULL) {
+		PyObject *res;
+		int ok = -1;
+		if (Py_EnterRecursiveCall(" in __subclasscheck__"))
+			return ok;
+		res = PyObject_CallFunctionObjArgs(checker, derived, NULL);
+		Py_LeaveRecursiveCall();
+		Py_DECREF(checker);
+		if (res != NULL) {
+			ok = PyObject_IsTrue(res);
+			Py_DECREF(res);
+		}
+		return ok;
+	}
     return recursive_issubclass(derived, cls, Py_GetRecursionLimit());
 }
 


More information about the Python-checkins mailing list