[Python-3000-checkins] r55837 - in python/branches/p3yk/Lib: abc.py test/test_abc.py
guido.van.rossum
python-3000-checkins at python.org
Sat Jun 9 01:04:48 CEST 2007
Author: guido.van.rossum
Date: Sat Jun 9 01:04:42 2007
New Revision: 55837
Added:
python/branches/p3yk/Lib/abc.py (contents, props changed)
python/branches/p3yk/Lib/test/test_abc.py (contents, props changed)
Log:
PEP 3119 -- the abc module.
Added: python/branches/p3yk/Lib/abc.py
==============================================================================
--- (empty file)
+++ python/branches/p3yk/Lib/abc.py Sat Jun 9 01:04:42 2007
@@ -0,0 +1,163 @@
+# Copyright 2007 Google, Inc. All Rights Reserved.
+# Licensed to PSF under a Contributor Agreement.
+
+"""Abstract Base Classes (ABCs) according to PEP 3119."""
+
+import sys
+import inspect
+import itertools
+
+
+### ABC SUPPORT FRAMEWORK ###
+
+
+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 _Abstract(object):
+
+ """Helper class inserted into the bases by ABCMeta (using _fix_bases()).
+
+ You should never need to explicitly subclass this class.
+ """
+
+ 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))))
+ return super(_Abstract, cls).__new__(cls, *args, **kwds)
+
+
+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.
+ __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 = {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.__registry = set()
+ cls.__cache = set()
+ cls.__negative_cache = set()
+ cls.__negative_cache_version = ABCMeta.__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.__registry.add(subclass)
+ ABCMeta.__invalidation_counter += 1 # Invalidate negative cache
+
+ def _dump_registry(cls, file=None):
+ """Debug helper to print the ABC registry."""
+ if file is None:
+ file = sys.stdout
+ print("Class: %s.%s" % (cls.__module__, cls.__name__), file=file)
+ print("Inv.counter: %s" % ABCMeta.__invalidation_counter, file=file)
+ for name in sorted(cls.__dict__.keys()):
+ if name.startswith("__abc_"):
+ value = getattr(cls, name)
+ print("%s: %r" % (name, value), file=file)
+
+ def __instancecheck__(cls, instance):
+ """Override for isinstance(instance, cls)."""
+ return any(cls.__subclasscheck__(c)
+ for c in {instance.__class__, type(instance)})
+
+ def __subclasscheck__(cls, subclass):
+ """Override for issubclass(subclass, cls)."""
+ # Check cache
+ if subclass in cls.__cache:
+ return True
+ # Check negative cache; may have to invalidate
+ if cls.__negative_cache_version < ABCMeta.__invalidation_counter:
+ # Invalidate the negative cache
+ cls.__negative_cache_version = ABCMeta.__invalidation_counter
+ cls.__negative_cache = set()
+ elif subclass in cls.__negative_cache:
+ return False
+ # Check if it's a direct subclass
+ if cls in subclass.__mro__:
+ cls.__cache.add(subclass)
+ return True
+ # Check if it's a subclass of a registered class (recursive)
+ for rcls in cls.__registry:
+ if issubclass(subclass, rcls):
+ cls.__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.__registry.add(subclass)
+ return True
+ # No dice; update negative cache
+ cls.__negative_cache.add(subclass)
+ return False
Added: python/branches/p3yk/Lib/test/test_abc.py
==============================================================================
--- (empty file)
+++ python/branches/p3yk/Lib/test/test_abc.py Sat Jun 9 01:04:42 2007
@@ -0,0 +1,123 @@
+# 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
+
+
+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_abstractmethod_integration(self):
+ class C(metaclass=abc.ABCMeta):
+ @abc.abstractmethod
+ def foo(self): pass # abstract
+ def bar(self): pass # concrete
+ self.assertEqual(C.__abstractmethods__, {"foo"})
+ self.assertRaises(TypeError, C) # because foo is abstract
+ class D(C):
+ def bar(self): pass # concrete override of concrete
+ self.assertEqual(D.__abstractmethods__, {"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):
+ @abc.abstractmethod
+ def bar(self): pass # abstract override of concrete
+ self.assertEqual(F.__abstractmethods__, {"bar"})
+ self.assertRaises(TypeError, F) # because bar is abstract now
+
+ def test_registration_basics(self):
+ class A(metaclass=abc.ABCMeta):
+ pass
+ 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):
+ pass
+ 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):
+ pass
+ 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):
+ pass
+ self.failUnless(issubclass(A, A))
+ class B(metaclass=abc.ABCMeta):
+ pass
+ self.failIf(issubclass(A, B))
+ self.failIf(issubclass(B, A))
+ class C(metaclass=abc.ABCMeta):
+ pass
+ 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()
More information about the Python-3000-checkins
mailing list