[pypy-svn] r54258 - in pypy/dist/pypy/objspace/std: . test

arigo at codespeak.net arigo at codespeak.net
Wed Apr 30 11:44:27 CEST 2008


Author: arigo
Date: Wed Apr 30 11:44:26 2008
New Revision: 54258

Modified:
   pypy/dist/pypy/objspace/std/multimethod.py
   pypy/dist/pypy/objspace/std/test/test_multimethod.py
Log:
Decide precisely how multimethods should interact with
RPython-level subclassing.  A minor fix in InstallerVersion1
is enough to ensure it works this way.


Modified: pypy/dist/pypy/objspace/std/multimethod.py
==============================================================================
--- pypy/dist/pypy/objspace/std/multimethod.py	(original)
+++ pypy/dist/pypy/objspace/std/multimethod.py	Wed Apr 30 11:44:26 2008
@@ -1,6 +1,29 @@
 
 from pypy.tool.sourcetools import compile2
 
+# This provide two compatible implementations of "multimethods".  A
+# multimethod is a callable object which chooses and calls a real
+# function from a table of pre-registered functions.  The choice depends
+# on the '__class__' of all arguments.  For example usages see
+# test_multimethod.
+
+# These multimethods support delegation: for each class A we must
+# provide a "typeorder", which is list of pairs ((B, converter)) where B
+# is a class and 'converter' is a function that can convert from an
+# instance of A to an instance of B.  If 'converter' is None it is
+# assumed that the instance needs no conversion.  The first entry in the
+# typeorder of a class A must almost always be (A, None).
+
+# A slightly non-standard feature of PyPy's multimethods is the way in
+# which they interact with normal subclassing.  Basically, they don't.
+# Suppose that A is a parent class of B.  Then a function registered for
+# an argument class A only accepts an instance whose __class__ is A, not
+# B.  To make it accept an instance of B, the typeorder for B must
+# contain (A, None).  An exception to this strict rule is if C is
+# another subclass of A which is not mentioned at all in the typeorder;
+# in this case C is considered to be equivalent to A.
+
+
 class FailedToImplement(Exception):
     def __init__(self, w_type=None, w_value=None):
         self.w_type  = w_type
@@ -121,6 +144,7 @@
         self.prefix = prefix
         self.prefix_memo[prefix] = 1
         self.list_of_typeorders = list_of_typeorders
+        self.check_typeorders()
         self.subtree_cache = {}
         self.to_install = []
         self.non_empty = self.build_tree([], multimethod.dispatch_tree)
@@ -135,6 +159,18 @@
         self.perform_call = self.build_function(None, prefix+'_perform_call',
                                                 None, perform)
 
+    def check_typeorders(self):
+        # xxx we use a '__'-separated list of the '__name__' of the types
+        # in build_single_method(), so types with the same __name__ or
+        # with '__' in them would obscurely break this logic
+        for typeorder in self.list_of_typeorders:
+            for type in typeorder:
+                assert '__' not in type.__name__, (
+                    "avoid '__' in the name of %r" % (type,))
+            names = dict.fromkeys([type.__name__ for type in typeorder])
+            assert len(names) == len(typeorder), (
+                "duplicate type.__name__ in %r" % (typeorder,))
+
     def is_empty(self):
         return not self.non_empty
 
@@ -143,9 +179,31 @@
         #print >> f, '_'*60
         #import pprint
         #pprint.pprint(self.list_of_typeorders, f)
+
+        def class_key(cls):
+            "Returns an object such that class_key(subcls) > class_key(cls)."
+            return len(cls.__mro__)
+
+        # Sort 'to_install' so that base classes come first, which is
+        # necessary for the 'parentfunc' logic in the loop below to work.
+        # Moreover, 'to_install' can contain two functions with the same
+        # name for the root class: the default fallback one and the real
+        # one.  So we have to sort the real one just after the default one
+        # so that the default one gets overridden.
+        def key(target, funcname, func, source, fallback):
+            if target is None:
+                return ()
+            return (class_key(target), not fallback)
+        self.to_install.sort(lambda a, b: cmp(key(*a), key(*b)))
+
         for target, funcname, func, source, fallback in self.to_install:
             if target is not None:
-                if hasattr(target, funcname) and fallback:
+                # If the parent class provides a method of the same
+                # name which is actually the same 'func', we don't need
+                # to install it again.  Useful with fallback functions.
+                parentfunc = getattr(target, funcname, None)
+                parentfunc = getattr(parentfunc, 'im_func', None)
+                if parentfunc is func:
                     continue
                 #print >> f, target.__name__, funcname
                 #if source:
@@ -194,13 +252,10 @@
                     if func is not None:
                         things_to_call.append((conversion, func, None))
 
-        if things_to_call:
-            funcname = intern(funcname)
-            self.build_function(next_type, funcname, len(types_so_far),
-                                things_to_call)
-            return True
-        else:
-            return False
+        funcname = intern(funcname)
+        self.build_function(next_type, funcname, len(types_so_far),
+                            things_to_call)
+        return bool(things_to_call)
 
     def build_function(self, target, funcname, func_selfarg_index,
                        things_to_call):
@@ -244,7 +299,9 @@
             miniglobals['raiseFailedToImplement'] = raiseFailedToImplement
             bodylines = ['return raiseFailedToImplement()']
             fallback = True
-
+            # NB. make sure that there is only one fallback function object,
+            # i.e. the key used in the mmfunccache below is always the same
+            # for all functions with the same name and an empty bodylines.
 
         # protect all lines apart from the last one by a try:except:
         for i in range(len(bodylines)-2, -1, -1):
@@ -291,10 +348,10 @@
 
 class MMDispatcher(object):
     """NOT_RPYTHON
-    Explicit dispatcher class.  This is not used in normal execution, which
-    uses the complex Installer below to install single-dispatch methods to
-    achieve the same result.  The MMDispatcher is only used by
-    rpython.lltypesystem.rmultimethod.  It is also nice for documentation.
+    Explicit dispatcher class.  The __call__ and dispatch() methods
+    are only present for documentation purposes.  The InstallerVersion2
+    uses the expressions() method to precompute fast RPython-friendly
+    dispatch tables.
     """
     _revcache = None
 
@@ -323,6 +380,9 @@
                 return v.function(*[expr(w) for w in v.arguments])
             else:
                 return v
+        # XXX this is incomplete: for each type in argtypes but not
+        # in the typeorder, we should look for the first base class
+        # that is in the typeorder.
         e = None
         for v in self.expressions(argtypes, prefixargs, args, suffixargs):
             try:

Modified: pypy/dist/pypy/objspace/std/test/test_multimethod.py
==============================================================================
--- pypy/dist/pypy/objspace/std/test/test_multimethod.py	(original)
+++ pypy/dist/pypy/objspace/std/test/test_multimethod.py	Wed Apr 30 11:44:26 2008
@@ -119,6 +119,45 @@
         raises(FailedToImplement, "add2(space, w_b2, w_s)")
         raises(FailedToImplement, "add2(space, w_s, w_b1)")
 
+    def test_forbidden_subclasses(self):
+        mul = multimethod.MultiMethodTable(2, root_class=W_Root,
+                                           argnames_before=['space'])
+        class UserW_StringObject(W_StringObject):
+            pass
+        def mul__Int_String(space, w_x, w_y):
+            assert space == 'space'
+            assert isinstance(w_x, W_IntObject)
+            assert isinstance(w_y, W_StringObject)
+            return 'fine'
+        mul.register(mul__Int_String, W_IntObject, W_StringObject)
+
+        mul1 = mul.install('__mul1', [self.typeorder, self.typeorder])
+        assert mul1('space', W_IntObject(), W_StringObject()) == 'fine'
+        assert mul1('space', W_IntObject(), UserW_StringObject()) == 'fine'
+
+        ext_typeorder = self.typeorder.copy()
+        ext_typeorder[UserW_StringObject] = []
+        mul2 = mul.install('__mul2', [ext_typeorder, ext_typeorder])
+        assert mul2('space', W_IntObject(), W_StringObject()) == 'fine'
+        raises(FailedToImplement,
+               mul2, 'baz', W_IntObject(), UserW_StringObject())
+
+    def test_ANY(self):
+        setattr = multimethod.MultiMethodTable(3, root_class=W_Root,
+                                           argnames_before=['space'])
+        def setattr__Int_ANY_ANY(space, w_x, w_y, w_z):
+            assert space == 'space'
+            assert isinstance(w_x, W_IntObject)
+            assert isinstance(w_y, W_Root)
+            assert isinstance(w_z, W_Root)
+            return w_y.__class__.__name__ + w_z.__class__.__name__
+        setattr.register(setattr__Int_ANY_ANY, W_IntObject, W_Root, W_Root)
+        setattr1 = setattr.install('__setattr1', [self.typeorder]*3)
+        for cls1 in self.typeorder:
+            for cls2 in self.typeorder:
+                assert setattr1('space', W_IntObject(), cls1(), cls2()) == (
+                    cls1.__name__ + cls2.__name__)
+
     def test_all_cases(self):
         import random
         space = 'space'



More information about the Pypy-commit mailing list