diff --git a/README.md b/README.md index fdc536d..b034a80 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,21 @@ function. Just like this: >>> assert 'test' not in dir(str) ``` +### Uncursing a curse + +If you want to restore your object as it was before a curse, you can use the `uncurse()` +function. Just like this: + +```python +>>> from datetime import datetime +>>> def mydate(self): +... return 'foo' +>>> curse(datetime, "now", classmethod(mydate)) +>>> assert datetime.now() == "foo" +>>> uncurse(datetime, "now") +>>> assert datetime.now() == datetime.datetime(...) +``` + ## Compatibility Forbidden Fruit runs on all cpython versions I tested so far, which includes diff --git a/forbiddenfruit/__init__.py b/forbiddenfruit/__init__.py index 9935684..e89c407 100644 --- a/forbiddenfruit/__init__.py +++ b/forbiddenfruit/__init__.py @@ -97,10 +97,9 @@ def curse(klass, attr, value, hide_from_dir=False): old_value = dikt.get(attr, None) old_name = '_c_%s' % attr # do not use .format here, it breaks py2.{5,6} - if old_value: - dikt[old_name] = old_value if old_value: + dikt[old_name] = old_value dikt[attr] = value try: @@ -113,6 +112,14 @@ def curse(klass, attr, value, hide_from_dir=False): pass else: dikt[attr] = value + + # This is needed, otherwise the unit tests blow up when using uncurse(). + # Not sure why. Maybe somebody wiser in the ways of Python internals + # will figure it out. All I know is that as long as __dict__ is + # accessed (either here or in the + # test_uncursing_a_builtin_class_with_a_class_method test), things work. + # This behavior was see in Python v2.7.10. + _ = klass.__dict__ if hide_from_dir: __hidden_elements__[klass.__name__].append(attr) @@ -161,3 +168,35 @@ def wrapper(func): curse(klass, name, func) return func return wrapper + + +def uncurse(klass, attr): + """Uncurse a cursed object + + This function restores an attribute to the condition it was before it was + cursed. Whereas reverse() will remove any specified attribute, uncurse() + will reset existing attributes back to the original value. + + Example: + + >>> from datetime import datetime + >>> from forbiddenfruit import curse, uncurse + >>> def mydate(self): + ... return 'foo' + >>> curse(datetime, 'now', classmethod(mydate)) + >>> datetime.now() + foo + >>> uncurse(datetime, 'now') + >>> datetime.now() + >>> datetime.datetime(...) + """ + + dikt = patchable_builtin(klass) + old_name = '_c_%s' % attr # do not use .format here, it breaks py2.{5,6} + + if old_name in dikt: + dikt[attr] = dikt[old_name] + del dikt[old_name] + else: + del dikt[attr] + diff --git a/setup.py b/setup.py index 2356aa7..eb7630a 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ if __name__ == '__main__': setup( name='forbiddenfruit', - version='0.1.1', + version='0.2.1', description='Patch python built-in objects', long_description=local_file('README.md'), author='Lincoln de Sousa', diff --git a/tests/unit/test_forbidden_fruit.py b/tests/unit/test_forbidden_fruit.py index 4e9fb3a..63acce8 100644 --- a/tests/unit/test_forbidden_fruit.py +++ b/tests/unit/test_forbidden_fruit.py @@ -1,11 +1,97 @@ from datetime import datetime -from forbiddenfruit import curses, curse, reverse +from forbiddenfruit import curses, curse, reverse, uncurse # Our stub! :) from . import ffruit -def test_cursing_a_builting_class(): +from datetime import datetime +from forbiddenfruit import curses, curse, reverse, uncurse + +# Our stub! :) +from . import ffruit + + +# NOTE: uncursing tests are done first as to not be corrupted by the other +# tests which do NOT restore the overwritten/added patched objects + + +def test_uncursing_a_builtin_class(): + + # Given that I have a function that returns *blah* + def words_of_widom(self): + return self * "blah " + + # And I curse a built-in class with that function + curse(int, "words_of_wisdom", words_of_widom) + + # And I see that the class was cursed + assert (2).words_of_wisdom() == "blah blah " + assert 'words_of_wisdom' in dir(int) + + # Then I uncurse that class + uncurse(int, "words_of_wisdom") + + # And I see that the class was uncursed + assert 'words_of_wisdom' not in dir(int) + + +def test_uncursing_a_builtin_class_with_a_class_method(): + # Given that I have a function that returns *blah* + def hello(self): + return "blah" + + # And I curse a built-in class with that function + curse(str, "hello", classmethod(hello)) + + # And I see that the class was cursed + assert str.hello() == "blah" + assert 'hello' in dir(str) + + # Then I uncurse that class + uncurse(str, "hello") + + # And I see that the class was uncursed + assert 'hello' not in dir(str) + + +def test_restoring_class_method(): + # Given that I havea cursed object + curse(datetime, 'now', classmethod(lambda *p: False)) + + # And I see that the method was replaced with the original method set + # as '_c_append' + assert '_c_now' in dir(datetime) + assert datetime.now() is False + assert datetime(2013, 4, 5).now() is False + + # Then I restore that method + uncurse(datetime, 'now') + + # And I see that the method was restored + assert '_c_now' not in dir(datetime) + assert datetime.now() is not False + assert datetime(2013, 4, 5).now() is not False + + +def test_restoring_instance_method(): + # Given that I have an instance of a `Dummy` object + obj = ffruit.Dummy() + + # And I curse an instance method + curse(ffruit.Dummy, "my_method", lambda *a, **k: "Yo!") + + # And I see that my object was cursed properly + assert obj.my_method() == "Yo!" + + # Then I restore the object + uncurse(ffruit.Dummy, "my_method") + + # And I see that the object was restored + assert obj.my_method() == ((), {}) + + +def test_cursing_a_builtin_class(): # Given that I have a function that returns *blah* def words_of_wisdom(self): @@ -19,7 +105,7 @@ def words_of_wisdom(self): assert 'words_of_wisdom' in dir(int) -def test_cursing_a_builting_class_with_a_class_method(): +def test_cursing_a_builtin_class_with_a_class_method(): # Given that I have a function that returns *blah* def hello(self):