temporal | 40ee551 | 2008-07-10 02:12:20 +0000 | [diff] [blame] | 1 | #!/usr/bin/python2.4 |
| 2 | # |
| 3 | # Copyright 2008 Google Inc. |
| 4 | # |
| 5 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 6 | # you may not use this file except in compliance with the License. |
| 7 | # You may obtain a copy of the License at |
| 8 | # |
| 9 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | # |
| 11 | # Unless required by applicable law or agreed to in writing, software |
| 12 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 | # See the License for the specific language governing permissions and |
| 15 | # limitations under the License. |
| 16 | |
| 17 | # This file is used for testing. The original is at: |
| 18 | # http://code.google.com/p/pymox/ |
| 19 | |
cclauss | 35c6927 | 2018-06-11 23:17:46 +0200 | [diff] [blame] | 20 | import inspect |
| 21 | |
| 22 | |
temporal | 40ee551 | 2008-07-10 02:12:20 +0000 | [diff] [blame] | 23 | class StubOutForTesting: |
| 24 | """Sample Usage: |
| 25 | You want os.path.exists() to always return true during testing. |
| 26 | |
| 27 | stubs = StubOutForTesting() |
| 28 | stubs.Set(os.path, 'exists', lambda x: 1) |
| 29 | ... |
| 30 | stubs.UnsetAll() |
| 31 | |
| 32 | The above changes os.path.exists into a lambda that returns 1. Once |
| 33 | the ... part of the code finishes, the UnsetAll() looks up the old value |
| 34 | of os.path.exists and restores it. |
| 35 | |
| 36 | """ |
| 37 | def __init__(self): |
| 38 | self.cache = [] |
| 39 | self.stubs = [] |
| 40 | |
| 41 | def __del__(self): |
| 42 | self.SmartUnsetAll() |
| 43 | self.UnsetAll() |
| 44 | |
| 45 | def SmartSet(self, obj, attr_name, new_attr): |
| 46 | """Replace obj.attr_name with new_attr. This method is smart and works |
| 47 | at the module, class, and instance level while preserving proper |
| 48 | inheritance. It will not stub out C types however unless that has been |
| 49 | explicitly allowed by the type. |
| 50 | |
| 51 | This method supports the case where attr_name is a staticmethod or a |
| 52 | classmethod of obj. |
| 53 | |
| 54 | Notes: |
| 55 | - If obj is an instance, then it is its class that will actually be |
| 56 | stubbed. Note that the method Set() does not do that: if obj is |
| 57 | an instance, it (and not its class) will be stubbed. |
| 58 | - The stubbing is using the builtin getattr and setattr. So, the __get__ |
| 59 | and __set__ will be called when stubbing (TODO: A better idea would |
| 60 | probably be to manipulate obj.__dict__ instead of getattr() and |
| 61 | setattr()). |
| 62 | |
| 63 | Raises AttributeError if the attribute cannot be found. |
| 64 | """ |
| 65 | if (inspect.ismodule(obj) or |
| 66 | (not inspect.isclass(obj) and obj.__dict__.has_key(attr_name))): |
| 67 | orig_obj = obj |
| 68 | orig_attr = getattr(obj, attr_name) |
| 69 | |
| 70 | else: |
| 71 | if not inspect.isclass(obj): |
| 72 | mro = list(inspect.getmro(obj.__class__)) |
| 73 | else: |
| 74 | mro = list(inspect.getmro(obj)) |
| 75 | |
| 76 | mro.reverse() |
| 77 | |
| 78 | orig_attr = None |
| 79 | |
| 80 | for cls in mro: |
| 81 | try: |
| 82 | orig_obj = cls |
| 83 | orig_attr = getattr(obj, attr_name) |
| 84 | except AttributeError: |
| 85 | continue |
| 86 | |
| 87 | if orig_attr is None: |
| 88 | raise AttributeError("Attribute not found.") |
| 89 | |
| 90 | # Calling getattr() on a staticmethod transforms it to a 'normal' function. |
| 91 | # We need to ensure that we put it back as a staticmethod. |
| 92 | old_attribute = obj.__dict__.get(attr_name) |
| 93 | if old_attribute is not None and isinstance(old_attribute, staticmethod): |
| 94 | orig_attr = staticmethod(orig_attr) |
| 95 | |
| 96 | self.stubs.append((orig_obj, attr_name, orig_attr)) |
| 97 | setattr(orig_obj, attr_name, new_attr) |
| 98 | |
| 99 | def SmartUnsetAll(self): |
| 100 | """Reverses all the SmartSet() calls, restoring things to their original |
| 101 | definition. Its okay to call SmartUnsetAll() repeatedly, as later calls |
| 102 | have no effect if no SmartSet() calls have been made. |
| 103 | |
| 104 | """ |
| 105 | self.stubs.reverse() |
| 106 | |
| 107 | for args in self.stubs: |
| 108 | setattr(*args) |
| 109 | |
| 110 | self.stubs = [] |
| 111 | |
| 112 | def Set(self, parent, child_name, new_child): |
| 113 | """Replace child_name's old definition with new_child, in the context |
| 114 | of the given parent. The parent could be a module when the child is a |
| 115 | function at module scope. Or the parent could be a class when a class' |
| 116 | method is being replaced. The named child is set to new_child, while |
| 117 | the prior definition is saved away for later, when UnsetAll() is called. |
| 118 | |
| 119 | This method supports the case where child_name is a staticmethod or a |
| 120 | classmethod of parent. |
| 121 | """ |
| 122 | old_child = getattr(parent, child_name) |
| 123 | |
| 124 | old_attribute = parent.__dict__.get(child_name) |
| 125 | if old_attribute is not None and isinstance(old_attribute, staticmethod): |
| 126 | old_child = staticmethod(old_child) |
| 127 | |
| 128 | self.cache.append((parent, old_child, child_name)) |
| 129 | setattr(parent, child_name, new_child) |
| 130 | |
| 131 | def UnsetAll(self): |
| 132 | """Reverses all the Set() calls, restoring things to their original |
| 133 | definition. Its okay to call UnsetAll() repeatedly, as later calls have |
| 134 | no effect if no Set() calls have been made. |
| 135 | |
| 136 | """ |
| 137 | # Undo calls to Set() in reverse order, in case Set() was called on the |
| 138 | # same arguments repeatedly (want the original call to be last one undone) |
| 139 | self.cache.reverse() |
| 140 | |
| 141 | for (parent, old_child, child_name) in self.cache: |
| 142 | setattr(parent, child_name, old_child) |
| 143 | self.cache = [] |