Repository: christophercrouzet/gorilla

Issue:
Ability to unregister and reload patches
Hi
Would it be possible to add an unregister_extensions() function? Or have the register_extensions function do a reload() of the packages passed into it? I think it would speed up developing with gorilla and help with troubleshooting
Thanks for the awesome module
Luke

Discussion:

Patch:
diff --git a/doc/core.rst b/doc/core.rst
index 2fcf062..67d8999 100644
--- a/doc/core.rst
+++ b/doc/core.rst
@@ -28,3 +28,7 @@ Core
 ----
 
 .. autofunction:: apply
+
+----
+
+.. autofunction:: revert
diff --git a/gorilla.py b/gorilla.py
index 81b877d..a72ae65 100644
--- a/gorilla.py
+++ b/gorilla.py
@@ -51,8 +51,14 @@ else:
 # Pattern for each internal attribute name.
 _PATTERN = '_gorilla_%s'
 
-# Pattern for the name of the overidden attributes to be stored.
-_ORIGINAL_NAME = _PATTERN % ('original_%s',)
+# Pattern for the flag expressing whether an attribute was created.
+_CREATED = _PATTERN % ('created_%s',)
+
+# Pattern for the ids of the original attributes stored.
+_ORIGINAL_IDS = _PATTERN % ('ids_%s',)
+
+# Pattern for each original attribute stored.
+_ORIGINAL_ITEM = _PATTERN % ('item_%s_%d',)
 
 # Attribute for the decorator data.
 _DECORATOR_DATA = _PATTERN % ('decorator_data',)
@@ -228,7 +234,7 @@ class Patch(object):
                 setattr(self, key, value)
 
 
-def apply(patch):
+def apply(patch, id='default'):
     """Apply a patch.
 
     The patch's :attr:`~Patch.obj` attribute is injected into the patch's
@@ -241,6 +247,9 @@ def apply(patch):
     ----------
     patch : gorilla.Patch
         Patch.
+    id : str
+        When applying a stack of patches on top of a same attribute, this
+        identifier allows to pinpoint a specific original attribute if needed.
 
     Raises
     ------
@@ -262,7 +271,8 @@ def apply(patch):
     try:
         target = get_attribute(patch.destination, patch.name)
     except AttributeError:
-        pass
+        created = _CREATED % (patch.name,)
+        setattr(patch.destination, created, True)
     else:
         if not settings.allow_hit:
             raise RuntimeError(
@@ -276,13 +286,54 @@ def apply(patch):
                 % (patch.name, patch.destination.__name__))
 
         if settings.store_hit:
-            original_name = _ORIGINAL_NAME % (patch.name,)
-            if not hasattr(patch.destination, original_name):
-                setattr(patch.destination, original_name, target)
+            original_ids = _ORIGINAL_IDS % (patch.name,)
+            ids = getattr(patch.destination, original_ids, ())
+
+            original_item = _ORIGINAL_ITEM % (patch.name, len(ids))
+            setattr(patch.destination, original_item, target)
+
+            ids += (id,)
+            setattr(patch.destination, original_ids, ids)
 
     setattr(patch.destination, patch.name, patch.obj)
 
 
+def revert(patch):
+    """Revert a patch.
+
+    Parameters
+    ----------
+    patch : gorilla.Patch
+        Patch.
+
+    Note
+    ----
+    This is only possible if the attribute :attr:`Settings.store_hit` was set
+    to ``True`` when applying the patch and overriding an existing attribute.
+    """
+    created = _CREATED % (patch.name,)
+    if getattr(patch.destination, created, False):
+        delattr(patch.destination, patch.name)
+        return
+
+    original_ids = _ORIGINAL_IDS % (patch.name,)
+    try:
+        ids = getattr(patch.destination, original_ids)
+        if not ids:
+            raise AttributeError
+    except AttributeError:
+        raise RuntimeError(
+                "Cannot revert the attribute named '%s' since the setting "
+                "'store_hit' was not set to True when applying the patch."
+                % (patch.destination.__name__,))
+
+    original_item = _ORIGINAL_ITEM % (patch.name, len(ids) - 1)
+    attr = getattr(patch.destination, original_item)
+    setattr(patch.destination, patch.name, attr)
+    delattr(patch.destination, original_item)
+    setattr(patch.destination, original_ids, ids[:-1])
+
+
 def patch(destination, name=None, settings=None):
     """Decorator to create a patch.
 
@@ -638,7 +689,7 @@ def get_attribute(obj, name):
                          % (type(obj), name))
 
 
-def get_original_attribute(obj, name):
+def get_original_attribute(obj, name, id='default'):
     """Retrieve an overriden attribute that has been stored.
 
     Parameters
@@ -647,6 +698,8 @@ def get_original_attribute(obj, name):
         Object to search the attribute in.
     name : str
         Name of the attribute.
+    id : str
+        Identifier of the original attribute to retrieve from the stack.
 
     Returns
     -------
@@ -662,7 +715,24 @@ def get_original_attribute(obj, name):
     --------
     :attr:`Settings.allow_hit`.
     """
-    return getattr(obj, _ORIGINAL_NAME % (name,))
+    original_ids = _ORIGINAL_IDS % (name,)
+    try:
+        ids = getattr(obj, original_ids)
+        if not ids:
+            raise AttributeError
+    except AttributeError:
+        raise AttributeError(
+            "Cannot retrieve the attribute named '%s' since the setting "
+            "'store_hit' was not set to True when applying the patch."
+            % (obj.__name__,))
+
+    for i, original_id in reversed(tuple(enumerate(ids))):
+        if original_id == id:
+            original_item = _ORIGINAL_ITEM % (name, i)
+            return getattr(obj, original_item)
+
+    raise AttributeError(
+        "No original attribute found  matching the id '%s'." % (id,))
 
 
 def get_decorator_data(obj, set_default=False):


Test patch:
diff --git a/tests/core/frommodule.py b/tests/core/frommodule.py
index 5d86cd6..0191c96 100644
--- a/tests/core/frommodule.py
+++ b/tests/core/frommodule.py
@@ -126,3 +126,13 @@ class Child(Parent):
         """frommodule.Child.__init__"""
         super(Child, self).__init__()
         self.child_value = "frommodule.Child.child_value"
+
+
+def stack_1():
+    original = gorilla.get_original_attribute(tests.core.tomodule, 'stack', id='first')()
+    return original + ("white",)
+
+
+def stack_2():
+    original = gorilla.get_original_attribute(tests.core.tomodule, 'stack', id='second')()
+    return original + ("red",)
diff --git a/tests/core/tomodule.py b/tests/core/tomodule.py
index f78cce4..5730e31 100644
--- a/tests/core/tomodule.py
+++ b/tests/core/tomodule.py
@@ -97,3 +97,7 @@ class Child(Parent):
         """tomodule.Child.__init__"""
         super(Child, self).__init__()
         self.child_value = "tomodule.Child.child_value"
+
+
+def stack():
+    return ("blue",)
diff --git a/tests/test_core.py b/tests/test_core.py
index f8163a7..6949af9 100755
--- a/tests/test_core.py
+++ b/tests/test_core.py
@@ -272,14 +272,6 @@ class CoreTest(GorillaTestCase):
             result = gorilla.get_attribute(destination, name)
             self.assertIs(result, obj)
 
-            # `gorilla.get_original_attribute` cannot be used here because it
-            # could return a bounded method, which would not compare as
-            # expected.
-            original = gorilla.get_attribute(destination,
-                                             '_gorilla_original_%s' % (name,))
-            self.assertIs(original, target)
-            self.assertIsNot(original, result)
-
             if source_path == '':
                 branch_count += 1
                 self.assertEqual(result.global_variable, "frommodule.global_variable")
@@ -396,6 +388,44 @@ class CoreTest(GorillaTestCase):
 
             self.tearDown()
 
+    def test_revert_patch(self):
+        self.setUp()
+
+        settings = gorilla.Settings(allow_hit=True)
+        destination = _tomodule
+        obj = _frommodule.global_variable
+
+        patch = gorilla.Patch(destination, 'dummy', obj, settings=settings)
+        gorilla.apply(patch)
+        self.assertEqual(_tomodule.dummy, "frommodule.global_variable")
+        gorilla.revert(patch)
+        self.assertRaises(AttributeError, getattr, _tomodule, 'dummy')
+
+
+        patch = gorilla.Patch(destination, 'global_variable', obj, settings=settings)
+        gorilla.apply(patch)
+        self.assertEqual(_tomodule.global_variable, "frommodule.global_variable")
+        gorilla.revert(patch)
+        self.assertEqual(_tomodule.global_variable, "tomodule.global_variable")
+
+        self.tearDown()
+
+    def test_stack_patches(self):
+        self.setUp()
+
+        settings = gorilla.Settings(allow_hit=True)
+        destination = _tomodule
+
+        patch = gorilla.Patch(_tomodule, 'stack', _frommodule.stack_1, settings=settings)
+        gorilla.apply(patch, id='first')
+
+        patch = gorilla.Patch(_tomodule, 'stack', _frommodule.stack_2, settings=settings)
+        gorilla.apply(patch, id='second')
+
+        self.assertEqual(_tomodule.stack(), ("blue", "white", "red"))
+
+        self.tearDown()
+
 
 if __name__ == '__main__':
     from tests.run import run
diff --git a/tests/test_utils.py b/tests/test_utils.py
index dac2a98..8eba12a 100755
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -413,9 +413,6 @@ class UtilsTest(GorillaTestCase):
         gorilla.apply(patch)
         self.assertIs(_unfold(gorilla.get_original_attribute(destination, name)), target)
 
-        gorilla.apply(patch)
-        self.assertIs(_unfold(gorilla.get_original_attribute(destination, name)), target)
-
     def test__get_members_1(self):
         members = gorilla._get_members(_frommodule)
         expected_members = [