Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added uncurse() function #17

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 41 additions & 2 deletions forbiddenfruit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)
Expand Down Expand Up @@ -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]

2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
92 changes: 89 additions & 3 deletions tests/unit/test_forbidden_fruit.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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):
Expand Down