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

Do not overwrite existing class and object attributes with sure properties #129

Merged
merged 3 commits into from
Feb 10, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- Python 3.7-dev support (allowed to fail)

### Fixed
- Do not overwrite existing attributes of objects with sure properties (when. should, ...). Refs #124, #128
- Do not overwrite existing class and instance attributes with sure properties (when. should, ...). Refs #127, #129
- Fix patched built-in `dir()` method. Refs #124, #128

## [v1.4.0]
### Added
Expand Down
46 changes: 43 additions & 3 deletions sure/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,7 @@ def chain(func):
setattr(AssertionBuilder, func.__name__, func)
return func


def chainproperty(func):
"""Extend sure with a custom chain property."""
func = assertionproperty(func)
Expand All @@ -949,10 +950,28 @@ def make_safe_property(method, name, should_be_property=True):
return method(None)

def deleter(method, self, *args, **kw):
del overwritten_object_handlers[(id(self), method.__name__)]
if isinstance(self, type):
# if the attribute has to be deleted from a class object
# we cannot use ``del self.__dict__[name]`` directly because we cannot
# modify a mappingproxy object. Thus, we have to delete it in our
# proxy __dict__.
del overwritten_object_handlers[(id(self), method.__name__)]
else:
# if the attribute has to be deleted from an instance object
# we are able to directly delete it from the object's __dict__.
del self.__dict__[name]

def setter(method, self, other):
overwritten_object_handlers[(id(self), method.__name__)] = other
if isinstance(self, type):
# if the attribute has to be set to a class object
# we cannot use ``self.__dict__[name] = other`` directly because we cannot
# modify a mappingproxy object. Thus, we have to set it in our
# proxy __dict__.
overwritten_object_handlers[(id(self), method.__name__)] = other
else:
# if the attribute has to be set to an instance object
# we are able to directly set it in the object's __dict__.
self.__dict__[name] = other

return builtins.property(
fget=method,
Expand All @@ -962,6 +981,16 @@ def setter(method, self, other):

def positive_assertion(name, prop=True):
def method(self):
# check if the given object already has an attribute with the
# given name. If yes return it instead of patching it.
try:
if name in self.__dict__:
return self.__dict__[name]
except AttributeError:
# we do not have an object with __dict__, thus
# it's safe to just continue and patch the `name`.
pass

overwritten_object_handler = overwritten_object_handlers.get((id(self), name), None)
if overwritten_object_handler:
return overwritten_object_handler
Expand All @@ -981,6 +1010,16 @@ def method(self):

def negative_assertion(name, prop=True):
def method(self):
# check if the given object already has an attribute with the
# given name. If yes return it instead of patching it.
try:
if name in self.__dict__:
return self.__dict__[name]
except AttributeError:
# we do not have an object with __dict__, thus
# it's safe to just continue and patch the `name`.
pass

overwritten_object_handler = overwritten_object_handlers.get((id(self), name), None)
if overwritten_object_handler:
return overwritten_object_handler
Expand Down Expand Up @@ -1032,7 +1071,8 @@ def _new_dir(*obj):
if len(obj) > 1:
raise TypeError('dir expected at most 1 arguments, got {0}'.format(len(obj)))

return sorted(set(old_dir(obj[0])).difference(POSITIVES + NEGATIVES))
patched = [x for x in old_dir(obj[0]) if isinstance(getattr(obj[0], x), AssertionBuilder)]
return sorted(set(old_dir(obj[0])).difference(patched))

builtins.dir = _new_dir

Expand Down
45 changes: 45 additions & 0 deletions tests/test_cpython_patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,51 @@ def test_it_works_with_objects():
{}.should.be.empty


def test_shouldnt_overwrite_class_attributes():
"""do not patch already existing class attributes with same name"""
class Foo(object):
when = 42
shouldnt = 43
bar = 'bar'

Foo.when.should.be.equal(42)
Foo.shouldnt.should.be.equal(43)
Foo.bar.should.be.equal('bar')

Foo.__dict__.should.contain('when')
Foo.__dict__.should.contain('shouldnt')
Foo.__dict__.should.contain('bar')

dir(Foo).should.contain('when')
dir(Foo).should.contain('shouldnt')
dir(Foo).should.contain('bar')
dir(Foo).shouldnt.contain('should')


def test_shouldnt_overwrite_instance_attributes():
"""do not patch already existing instance attributes with same name"""
class Foo(object):
def __init__(self, when, shouldnt, bar):
self.when = when
self.shouldnt = shouldnt
self.bar = bar

f = Foo(42, 43, 'bar')

f.when.should.be.equal(42)
f.shouldnt.should.be.equal(43)
f.bar.should.be.equal('bar')

f.__dict__.should.contain('when')
f.__dict__.should.contain('shouldnt')
f.__dict__.should.contain('bar')

dir(f).should.contain('when')
dir(f).should.contain('shouldnt')
dir(f).should.contain('bar')
dir(f).shouldnt.contain('should')


def test_dir_conceals_sure_specific_attributes():
("dir(obj) should conceal names of methods that were grafted by sure")

Expand Down