Skip to content

Commit e86e059

Browse files
authored
Merge pull request #17620 from tacaswell/simplified_setattr_cm_method_handling
MNT: make _setattr_cm more conservative
2 parents c3bfeb9 + 9e88fa5 commit e86e059

File tree

2 files changed

+89
-2
lines changed

2 files changed

+89
-2
lines changed

lib/matplotlib/cbook/__init__.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2032,13 +2032,30 @@ def _setattr_cm(obj, **kwargs):
20322032
Temporarily set some attributes; restore original state at context exit.
20332033
"""
20342034
sentinel = object()
2035-
origs = [(attr, getattr(obj, attr, sentinel)) for attr in kwargs]
2035+
origs = {}
2036+
for attr in kwargs:
2037+
orig = getattr(obj, attr, sentinel)
2038+
2039+
if attr in obj.__dict__ or orig is sentinel:
2040+
origs[attr] = orig
2041+
else:
2042+
cls_orig = getattr(type(obj), attr)
2043+
if isinstance(cls_orig, property):
2044+
origs[attr] = orig
2045+
elif isinstance(cls_orig, types.FunctionType):
2046+
origs[attr] = sentinel
2047+
else:
2048+
raise ValueError(
2049+
f"trying to set {attr} which is not a method, "
2050+
"property, or instance level attribute"
2051+
)
2052+
20362053
try:
20372054
for attr, val in kwargs.items():
20382055
setattr(obj, attr, val)
20392056
yield
20402057
finally:
2041-
for attr, orig in origs:
2058+
for attr, orig in origs.items():
20422059
if orig is sentinel:
20432060
delattr(obj, attr)
20442061
else:

lib/matplotlib/tests/test_cbook.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,3 +651,73 @@ 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+
other = A()
681+
682+
def verify_pre_post_state(obj):
683+
# When you access a Python method the function is bound
684+
# to the object at access time so you get a new instance
685+
# of MethodType every time.
686+
#
687+
# https://docs.python.org/3/howto/descriptor.html#functions-and-methods
688+
assert obj.meth is not obj.meth
689+
# normal attribute should give you back the same instance every time
690+
assert obj.aardvark is obj.aardvark
691+
assert a.aardvark == 'aardvark'
692+
# and our property happens to give the same instance every time
693+
assert obj.prop is obj.prop
694+
assert obj.cls_level is A.cls_level
695+
assert obj.override == 'override'
696+
assert not hasattr(obj, 'extra')
697+
assert obj.prop == 'p'
698+
assert obj.monkey == other.meth
699+
700+
a = B()
701+
702+
a.monkey = other.meth
703+
verify_pre_post_state(a)
704+
with cbook._setattr_cm(
705+
a, prop='squirrel',
706+
aardvark='moose', meth=lambda: None,
707+
override='boo', extra='extra',
708+
monkey=lambda: None):
709+
# because we have set a lambda, it is normal attribute access
710+
# and the same every time
711+
assert a.meth is a.meth
712+
assert a.aardvark is a.aardvark
713+
assert a.aardvark == 'moose'
714+
assert a.override == 'boo'
715+
assert a.extra == 'extra'
716+
assert a.prop == 'squirrel'
717+
assert a.monkey != other.meth
718+
719+
verify_pre_post_state(a)
720+
721+
with pytest.raises(ValueError):
722+
with cbook._setattr_cm(a, cls_level='bob'):
723+
pass

0 commit comments

Comments
 (0)