Skip to content

Commit 4111ab6

Browse files
committed
MNT: make _setattr_cm more conservative
- special case methods because they are descriptors - fail on any other non-instance attribute
1 parent 21c3acb commit 4111ab6

File tree

2 files changed

+88
-2
lines changed

2 files changed

+88
-2
lines changed

lib/matplotlib/cbook/__init__.py

+22-2
Original file line numberDiff line numberDiff line change
@@ -2031,14 +2031,34 @@ def _setattr_cm(obj, **kwargs):
20312031
"""
20322032
Temporarily set some attributes; restore original state at context exit.
20332033
"""
2034+
import inspect
20342035
sentinel = object()
2035-
origs = [(attr, getattr(obj, attr, sentinel)) for attr in kwargs]
2036+
origs = {}
2037+
for attr in kwargs:
2038+
orig = getattr(obj, attr, sentinel)
2039+
# monkey patching on a new attribute, this is OK
2040+
if orig is sentinel:
2041+
origs[attr] = sentinel
2042+
# special case methods
2043+
elif inspect.ismethod(orig):
2044+
origs[attr] = sentinel
2045+
else:
2046+
# if we are trying to monkey patch a non-instance attribute,
2047+
# fail so we don't have to sort out how to safely identify
2048+
# general descriptors.
2049+
if attr not in obj.__dict__:
2050+
raise ValueError(
2051+
f"trying to set {attr} which is not a method or "
2052+
"instance level attribute"
2053+
)
2054+
origs[attr] = orig
2055+
20362056
try:
20372057
for attr, val in kwargs.items():
20382058
setattr(obj, attr, val)
20392059
yield
20402060
finally:
2041-
for attr, orig in origs:
2061+
for attr, orig in origs.items():
20422062
if orig is sentinel:
20432063
delattr(obj, attr)
20442064
else:

lib/matplotlib/tests/test_cbook.py

+66
Original file line numberDiff line numberDiff line change
@@ -651,3 +651,69 @@ def test_check_shape(target, test_shape):
651651
with pytest.raises(ValueError,
652652
match=error_pattern):
653653
cbook._check_shape(target, aardvark=data)
654+
655+
656+
def test_setattr_cm():
657+
class A:
658+
659+
cls_level = object()
660+
override = object()
661+
def __init__(self):
662+
self.aardvark = 'aardvark'
663+
self.override = 'override'
664+
self._p = 'p'
665+
666+
def meth(self):
667+
...
668+
669+
@property
670+
def prop(self):
671+
return self._p
672+
673+
@prop.setter
674+
def prop(self, val):
675+
self._p = val
676+
677+
class B(A):
678+
...
679+
680+
def verify_pre_post_state(obj):
681+
# When you access a Python method the function is bound
682+
# to the object at access time so you get a new instance
683+
# of MethodType every time.
684+
#
685+
# https://docs.python.org/3/howto/descriptor.html#functions-and-methods
686+
assert obj.meth is not obj.meth
687+
# normal attribute should give you back the same
688+
# instance every time
689+
assert obj.aardvark is obj.aardvark
690+
assert a.aardvark == 'aardvark'
691+
# and our property happens to give the same instance every time
692+
assert obj.prop is obj.prop
693+
assert obj.cls_level is A.cls_level
694+
assert obj.override == 'override'
695+
assert not hasattr(obj, 'extra')
696+
697+
a = B()
698+
verify_pre_post_state(a)
699+
with cbook._setattr_cm(
700+
a,
701+
aardvark='moose', meth=lambda: None,
702+
override='boo', extra='extra'
703+
):
704+
# because we have set a lambda, it is normal attribute access
705+
# and the same every time
706+
assert a.meth is a.meth
707+
assert a.aardvark is a.aardvark
708+
assert a.aardvark == 'moose'
709+
assert a.override == 'boo'
710+
assert a.extra == 'extra'
711+
verify_pre_post_state(a)
712+
713+
with pytest.raises(ValueError):
714+
with cbook._setattr_cm(a, prop=''):
715+
...
716+
717+
with pytest.raises(ValueError):
718+
with cbook._setattr_cm(a, cls_level='bob'):
719+
...

0 commit comments

Comments
 (0)