tests/pymox/stubout.py
changeset 1000 9af147fc1f1c
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/pymox/stubout.py	Mon Jan 26 16:04:19 2009 +0000
@@ -0,0 +1,139 @@
+#!/usr/bin/python2.4
+#
+# Copyright 2008 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import inspect
+
+class StubOutForTesting:
+  """Sample Usage:
+     You want os.path.exists() to always return true during testing.
+
+     stubs = StubOutForTesting()
+     stubs.Set(os.path, 'exists', lambda x: 1)
+       ...
+     stubs.UnsetAll()
+
+     The above changes os.path.exists into a lambda that returns 1.  Once
+     the ... part of the code finishes, the UnsetAll() looks up the old value
+     of os.path.exists and restores it.
+
+  """
+  def __init__(self):
+    self.cache = []
+    self.stubs = []
+
+  def __del__(self):
+    self.SmartUnsetAll()
+    self.UnsetAll()
+
+  def SmartSet(self, obj, attr_name, new_attr):
+    """Replace obj.attr_name with new_attr. This method is smart and works
+       at the module, class, and instance level while preserving proper
+       inheritance. It will not stub out C types however unless that has been
+       explicitly allowed by the type.
+
+       This method supports the case where attr_name is a staticmethod or a
+       classmethod of obj.
+
+       Notes:
+      - If obj is an instance, then it is its class that will actually be
+        stubbed. Note that the method Set() does not do that: if obj is
+        an instance, it (and not its class) will be stubbed.
+      - The stubbing is using the builtin getattr and setattr. So, the __get__
+        and __set__ will be called when stubbing (TODO: A better idea would
+        probably be to manipulate obj.__dict__ instead of getattr() and
+        setattr()).
+
+       Raises AttributeError if the attribute cannot be found.
+    """
+    if (inspect.ismodule(obj) or
+        (not inspect.isclass(obj) and obj.__dict__.has_key(attr_name))):
+      orig_obj = obj
+      orig_attr = getattr(obj, attr_name)
+
+    else:
+      if not inspect.isclass(obj):
+        mro = list(inspect.getmro(obj.__class__))
+      else:
+        mro = list(inspect.getmro(obj))
+
+      mro.reverse()
+
+      orig_attr = None
+
+      for cls in mro:
+        try:
+          orig_obj = cls
+          orig_attr = getattr(obj, attr_name)
+        except AttributeError:
+          continue
+
+    if orig_attr is None:
+      raise AttributeError("Attribute not found.")
+
+    # Calling getattr() on a staticmethod transforms it to a 'normal' function.
+    # We need to ensure that we put it back as a staticmethod.
+    old_attribute = obj.__dict__.get(attr_name)
+    if old_attribute is not None and isinstance(old_attribute, staticmethod):
+      orig_attr = staticmethod(orig_attr)
+
+    self.stubs.append((orig_obj, attr_name, orig_attr))
+    setattr(orig_obj, attr_name, new_attr)
+
+  def SmartUnsetAll(self):
+    """Reverses all the SmartSet() calls, restoring things to their original
+    definition.  Its okay to call SmartUnsetAll() repeatedly, as later calls
+    have no effect if no SmartSet() calls have been made.
+
+    """
+    self.stubs.reverse()
+
+    for args in self.stubs:
+      setattr(*args)
+
+    self.stubs = []
+
+  def Set(self, parent, child_name, new_child):
+    """Replace child_name's old definition with new_child, in the context
+    of the given parent.  The parent could be a module when the child is a
+    function at module scope.  Or the parent could be a class when a class'
+    method is being replaced.  The named child is set to new_child, while
+    the prior definition is saved away for later, when UnsetAll() is called.
+
+    This method supports the case where child_name is a staticmethod or a
+    classmethod of parent.
+    """
+    old_child = getattr(parent, child_name)
+
+    old_attribute = parent.__dict__.get(child_name)
+    if old_attribute is not None and isinstance(old_attribute, staticmethod):
+      old_child = staticmethod(old_child)
+
+    self.cache.append((parent, old_child, child_name))
+    setattr(parent, child_name, new_child)
+
+  def UnsetAll(self):
+    """Reverses all the Set() calls, restoring things to their original
+    definition.  Its okay to call UnsetAll() repeatedly, as later calls have
+    no effect if no Set() calls have been made.
+
+    """
+    # Undo calls to Set() in reverse order, in case Set() was called on the
+    # same arguments repeatedly (want the original call to be last one undone)
+    self.cache.reverse()
+
+    for (parent, old_child, child_name) in self.cache:
+      setattr(parent, child_name, old_child)
+    self.cache = []