[Python-checkins] r55315 - sandbox/trunk/abc/abc.py sandbox/trunk/abc/test_abc.py
guido.van.rossum
python-checkins at python.org
Mon May 14 20:56:16 CEST 2007
Author: guido.van.rossum
Date: Mon May 14 20:56:14 2007
New Revision: 55315
Modified:
sandbox/trunk/abc/abc.py
sandbox/trunk/abc/test_abc.py
Log:
Move to the new ABCMeta metaclass machinery.
Specify the hash function for sets.
Modified: sandbox/trunk/abc/abc.py
==============================================================================
--- sandbox/trunk/abc/abc.py (original)
+++ sandbox/trunk/abc/abc.py Mon May 14 20:56:14 2007
@@ -1,15 +1,9 @@
#!/usr/bin/env python3.0
-"""Abstract Base Classes experiment. See PEP 3119.
+"""Abstract Base Classes (ABCs) experiment. See PEP 3119.
-Note: this depends on the brand new Py3k feature that object.__ne__()
-is implemented by calling __eq__() and reversing the outcome (unless
-NotImplemented).
-
-XXX Should we use argument annotations here?
-
-XXX How to decide the order in which orthogonal base classes are
-listed? For now, I'm putting the smaller API second.
+Note: this code depends on an unsubmitted patch:
+http://python.org/sf/1708353.
"""
__author__ = "Guido van Rossum <guido at python.org>"
@@ -21,66 +15,31 @@
### ABC SUPPORT FRAMEWORK ###
-# Note: @abstractmethod will become a built-in; Abstract and
-# AbstractClass will disappear (the functionality will be subsumed in
-# object and type, respectively).
-
-
def abstractmethod(funcobj):
"""A decorator indicating abstract methods.
- Requires that the class (directly or indirectly) derives from
- Abstract, and that the metaclass is AbstractClass or derived from it
- (deriving from Abstract ensure this). A class deriving from
- Abstract 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.
+ 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(Abstract):
+ class C(metaclass=ABCMeta):
@abstractmethod
def my_abstract_method(self, ...):
...
-
- When combining this with other decorators, this should come last:
-
- class C(Abstract):
- @classmethod
- @abstractmethod
- def my_abstract_class_method(self, ...):
- ...
-
- XXX This example doesn't currently work, since classmethod doesn't
- pass through function attributes! I think it should though.
"""
funcobj.__isabstractmethod__ = True
return funcobj
-class AbstractClass(type):
-
- """Metaclass to support the abstractmethod decorator."""
-
- def __new__(mcls, name, bases, namespace):
- cls = super(AbstractClass, mcls).__new__(mcls, name, bases, namespace)
- 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
- return cls
-
-
-class Abstract(metaclass=AbstractClass):
+class _Abstract(object):
- """Base class to support the abstractmethod decorator.
+ """Helper class inserted into the bases by ABCMeta (using _fix_bases()).
- This implicitly sets the metaclass to AbstractClass.
+ You should never need to explicitly subclass this class.
"""
def __new__(cls, *args, **kwds):
@@ -89,52 +48,129 @@
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)
-
-
-### ORDERING ABCS ###
-
+ return super(_Abstract, cls).__new__(cls, *args, **kwds)
-class PartiallyOrdered(Abstract):
- """Partial orderings define a consistent but incomplete < operator.
+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()).
- Invariant: a < b < c => a < c
-
- It is possible that none of a < b, a == b, a > b hold.
"""
- @abstractmethod
- def __lt__(self, other):
- return NotImplemented
-
- def __le__(self, other):
- if not isinstance(other, PartiallyOrdered):
- return NotImplemented
- # Note that bool(NotImplemented) is True!
- return self == other or self.__lt__(other)
-
- # It's not necessary to define __gt__ and __ge__; these will
- # automatically be translated to __lt__ and __le__ calls with
- # swapped arguments by the rich comparisons framework.
-
-
-class TotallyOrdered(PartiallyOrdered):
+ # 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
- """Total orderings guarantee that all values are ordered.
-
- E.g. for any two a and b, exactly one of a < b, a == b, a > b holds.
+ 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.__abc_registry__ = set()
+ cls.__abc_cache__ = set()
+ cls.__abc_negative_cache__ = set()
+ cls.__abc_negative_cache_version__ = ABCMeta.__invalidation_counter
+ return cls
- XXX What about float? The properties of NaN make it strictly
- PartiallyOrdered. But having it TotallyOrdered makes more sense
- for most practical purposes.
- """
+ 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.__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.__abc_cache__:
+ return True
+ # Check negative cache; may have to invalidate
+ if cls.__abc_negative_cache_version__ < ABCMeta.__invalidation_counter:
+ # Invalidate the negative cache
+ cls.__abc_negative_cache_version__ = ABCMeta.__invalidation_counter
+ cls.__abc_negative_cache__ = set()
+ elif subclass in cls.__abc_negative_cache__:
+ return False
+ # 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
### ONE TRICK PONIES ###
-class Hashable(Abstract):
+class Hashable(metaclass=ABCMeta):
"""A hashable has one method, __hash__()."""
@@ -143,7 +179,7 @@
return 0
-class Iterable(Abstract):
+class Iterable(metaclass=ABCMeta):
"""An iterable has one method, __iter__()."""
@@ -175,14 +211,14 @@
# Or: raise StopIteration
-class Sized(Abstract):
+class Sized(metaclass=ABCMeta):
@abstractmethod
def __len__(self):
return 0
-class Container(Abstract):
+class Container(metaclass=ABCMeta):
"""A container has a __contains__() method."""
@@ -198,7 +234,7 @@
# Perhaps later, when we have type annotations so you can write
# Container[T], we can do this:
#
- # class Container(Abstract):
+ # class Container(metaclass=ABCMeta):
# def __contains__(self, val: T) -> bool: ...
#
# class Searchable(Container):
@@ -278,8 +314,22 @@
freedom for __eq__ or __hash__. We match the algorithm used
by the built-in frozenset type.
"""
- # XXX This is not a very efficient implementation
- return hash(frozenset(self))
+ MAX = sys.maxint
+ MASK = 2 * MAX + 1
+ n = len(self)
+ h = 1927868237 * (n + 1)
+ h &= MASK
+ for x in self:
+ hx = hash(x)
+ h ^= (hx ^ (hx << 16) ^ 89869747) * 3644798167
+ h &= MASK
+ h = h * 69069 + 907133923
+ h &= MASK
+ if h > MAX:
+ h -= MASK + 1
+ if h == -1:
+ h = 590923713
+ return h
# XXX Should this derive from Set instead of from ComposableSet?
@@ -505,10 +555,6 @@
return False
-# XXX HashableMapping
-# XXX MutableMapping
-
-
### SEQUENCES ###
@@ -662,31 +708,6 @@
return len(self) <= len(other)
-class HashableSequence(Sequence, Hashable):
-
- def __hash__(self):
- """The hash value must match __eq__.
-
- Since we want sequences to be equal iff their elements are equal
- (regardless of the sequence type), this algorithm is (XXX
- hopefully) identical to tuple.__hash__().
- """
- mask = sys.maxint*2 + 1
- mult = 1000003
- h = 0x345678
- n = len(self)
- for i, elem in enumerate(self):
- h = ((h ^ hash(elem)) * mult) & mask
- mult = (mult + (82520 - 2 + 2*(n-i))) & mask
- h += 97531
- h &= mask
- if h > sys.maxint:
- h -= mask + 1
- if h == -1:
- h = -2
- return h
-
-
### ADAPTERS ###
Modified: sandbox/trunk/abc/test_abc.py
==============================================================================
--- sandbox/trunk/abc/test_abc.py (original)
+++ sandbox/trunk/abc/test_abc.py Mon May 14 20:56:14 2007
@@ -9,7 +9,7 @@
class ABCTestCase(unittest.TestCase):
def test_abstract_method_machinery(self):
- class C(abc.Abstract):
+ class C(metaclass=abc.ABCMeta):
@abc.abstractmethod
def foo(self): pass
def bar(self): pass
@@ -21,22 +21,25 @@
def foo(self): pass
E()
- def test_hashable_sequence_hash_matches_tuple_hash(self):
- class C(abc.HashableSequence):
+ def test_hashable_set_hash_matches_frozenset_hash(self):
+ class C(abc.Set):
def __new__(cls, values):
obj = super(C, cls).__new__(cls)
- obj.__values = list(values)
+ obj.__values = set(values)
return obj
def __len__(self):
return len(self.__values)
- def __getitem__(self, i):
- return self.__values[i]
+ def __contains__(self, x):
+ return x in self.__values
+ def __iter__(self):
+ return iter(self.__values)
for l in ([], [0], [1], [0, 1],
list(range(-sys.maxint, sys.maxint, 100000)),
"The quick brown fox jumps over the lazy dog".split()):
- hcl = hash(C(l))
- htl = hash(tuple(l))
- self.assertEqual(hcl, htl, repr((l, hcl, htl)))
+ hc = C(l)._hash()
+ hfs = hash(frozenset(l))
+ self.assertEqual(hc, hfs, repr((l, hc, hfs)))
+
def test_adapt_to_sequence(self):
a = abc.AdaptToSequence(range(10))
@@ -45,7 +48,6 @@
self.assertEqual(a[9], 9)
self.assertEqual(a[-1], 9)
self.assertEqual(list(a), list(range(10)))
- #self.assertEqual(a, range(10))
b = a[1:-1]
self.assertEqual(b.__class__, abc.AdaptToSequence)
self.assertEqual(list(b), list(range(1, 9)))
More information about the Python-checkins
mailing list