Skip to content

MNT: make _setattr_cm more forgiving #17649

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

Merged
merged 2 commits into from
Jun 16, 2020
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions lib/matplotlib/cbook/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2035,20 +2035,28 @@ def _setattr_cm(obj, **kwargs):
origs = {}
for attr in kwargs:
orig = getattr(obj, attr, sentinel)

if attr in obj.__dict__ or orig is sentinel:
# if we are pulling from the instance dict or the object
# does not have this attribute we can trust the above
origs[attr] = orig
else:
# if the attribute is not in the instance dict it must be
# from the class level
cls_orig = getattr(type(obj), attr)
# if we are dealing with a property (but not a general descriptor)
# we want to set the original value back.
if isinstance(cls_orig, property):
origs[attr] = orig
elif isinstance(cls_orig, types.FunctionType):
origs[attr] = sentinel
# otherwise this is _something_ we are going to shadow at
# the instance dict level from higher up in the MRO. We
# are going to assume we can delattr(obj, attr) to clean
# up after ourselves. It is possible that this code will
# fail if used with a non-property custom descriptor which
# implements __set__ (and __delete__ does not act like a
# stack). However, this is an internal tool and we do not
# currently have any custom descriptors.
else:
raise ValueError(
f"trying to set {attr} which is not a method, "
"property, or instance level attribute"
)
origs[attr] = sentinel

try:
for attr, val in kwargs.items():
Expand Down
22 changes: 17 additions & 5 deletions lib/matplotlib/tests/test_cbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,14 @@ def __init__(self):
def meth(self):
...

@classmethod
def classy(klass):
...

@staticmethod
def static():
...

@property
def prop(self):
return self._p
Expand Down Expand Up @@ -696,6 +704,10 @@ def verify_pre_post_state(obj):
assert not hasattr(obj, 'extra')
assert obj.prop == 'p'
assert obj.monkey == other.meth
assert obj.cls_level is A.cls_level
assert 'cls_level' not in obj.__dict__
assert 'classy' not in obj.__dict__
assert 'static' not in obj.__dict__

a = B()

Expand All @@ -705,7 +717,8 @@ def verify_pre_post_state(obj):
a, prop='squirrel',
aardvark='moose', meth=lambda: None,
override='boo', extra='extra',
monkey=lambda: None):
monkey=lambda: None, cls_level='bob',
classy='classy', static='static'):
# because we have set a lambda, it is normal attribute access
# and the same every time
assert a.meth is a.meth
Expand All @@ -715,9 +728,8 @@ def verify_pre_post_state(obj):
assert a.extra == 'extra'
assert a.prop == 'squirrel'
assert a.monkey != other.meth
assert a.cls_level == 'bob'
assert a.classy == 'classy'
assert a.static == 'static'

verify_pre_post_state(a)

with pytest.raises(ValueError):
with cbook._setattr_cm(a, cls_level='bob'):
pass