Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 89694ed

Browse files
committed
Prevent reuse of certain Locators and Formatters over multiple Axises.
The call to _wrap_locator_formatter was deleted from ThetaAxis.gca() because it is redundant with the call in ThetaAxis._set_scale (which gca() calls), and would cause double wrapping with _AxisWrapper, causing issues when checking for axis equality.
1 parent ac07e81 commit 89694ed

File tree

4 files changed

+83
-7
lines changed

4 files changed

+83
-7
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
Axis-dependent Locators and Formatters explicitly error out when used over multiple Axis
2+
````````````````````````````````````````````````````````````````````````````````````````
3+
4+
Certain Locators and Formatters (e.g. the default `AutoLocator` and
5+
`ScalarFormatter`) can only be used meaningfully on one Axis object at a time
6+
(i.e., attempting to use a single `AutoLocator` instance on the x and the y
7+
axis of an Axes, or the x axis of two different Axes, would result in
8+
nonsensical results).
9+
10+
Such "double-use" is now detected and raises a RuntimeError *at canvas draw
11+
time*. The exception is not raised when the second Axis is registered in order
12+
to avoid incorrectly raising exceptions for the Locators and Formatters that
13+
*can* be used on multiple Axis objects simultaneously (e.g. `NullLocator` and
14+
`FuncFormatter`).

lib/matplotlib/projections/polar.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,14 @@ class _AxisWrapper(object):
198198
def __init__(self, axis):
199199
self._axis = axis
200200

201+
def __eq__(self, other):
202+
# Needed so that assignment, as the locator.axis attribute, of another
203+
# _AxisWrapper wrapping the same axis works.
204+
return self._axis == other._axis
205+
206+
def __hash__(self):
207+
return hash((type(self), tuple(sorted(vars(self).items()))))
208+
201209
def get_view_interval(self):
202210
return np.rad2deg(self._axis.get_view_interval())
203211

@@ -227,10 +235,11 @@ class ThetaLocator(mticker.Locator):
227235
"""
228236
def __init__(self, base):
229237
self.base = base
230-
self.axis = self.base.axis = _AxisWrapper(self.base.axis)
238+
self.set_axis(self.base.axis)
231239

232240
def set_axis(self, axis):
233-
self.axis = _AxisWrapper(axis)
241+
super().set_axis(_AxisWrapper(axis))
242+
self.base._set_axises = set() # Bypass prevention of axis resetting.
234243
self.base.set_axis(self.axis)
235244

236245
def __call__(self):
@@ -383,7 +392,6 @@ def _wrap_locator_formatter(self):
383392
def cla(self):
384393
super().cla()
385394
self.set_ticks_position('none')
386-
self._wrap_locator_formatter()
387395

388396
def _set_scale(self, value, **kwargs):
389397
super()._set_scale(value, **kwargs)

lib/matplotlib/tests/test_ticker.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -897,3 +897,29 @@ def minorticksubplot(xminor, yminor, i):
897897
minorticksubplot(True, False, 2)
898898
minorticksubplot(False, True, 3)
899899
minorticksubplot(True, True, 4)
900+
901+
902+
def test_multiple_assignment():
903+
fig = plt.figure()
904+
905+
ax = fig.subplots()
906+
fmt = mticker.NullFormatter()
907+
ax.xaxis.set_major_formatter(fmt)
908+
ax.yaxis.set_major_formatter(fmt)
909+
fig.canvas.draw() # No error.
910+
fig.clf()
911+
912+
ax = fig.subplots()
913+
fmt = mticker.ScalarFormatter()
914+
ax.xaxis.set_major_formatter(fmt)
915+
ax.xaxis.set_minor_formatter(fmt)
916+
fig.canvas.draw() # No error.
917+
fig.clf()
918+
919+
ax = fig.subplots()
920+
fmt = mticker.ScalarFormatter()
921+
ax.xaxis.set_major_formatter(fmt)
922+
ax.yaxis.set_major_formatter(fmt)
923+
with pytest.raises(RuntimeError):
924+
fig.canvas.draw()
925+
fig.clf()

lib/matplotlib/ticker.py

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -217,11 +217,39 @@ def get_tick_space(self):
217217
return 9
218218

219219

220-
class TickHelper(object):
221-
axis = None
220+
class TickHelper:
221+
# TickHelpers that access their axis attribute can only be assigned to
222+
# one Axis at a time, but we don't know a priori whether they will (e.g.,
223+
# NullFormatter doesn't, but ScalarFormatter does). So keep track of all
224+
# Axises that a TickHelper is assigned to (in a set: a TickHelper could be
225+
# assigned both as major and minor helper on a single axis), but only error
226+
# out after multiple assignment when the attribute is accessed.
222227

223-
def set_axis(self, axis):
224-
self.axis = axis
228+
@property
229+
def axis(self):
230+
# We can't set the '_set_axises' attribute in TickHelper.__init__
231+
# (without a deprecation period) because subclasses didn't have to call
232+
# super().__init__ so far so they likely didn't.
233+
set_axises = getattr(self, "_set_axises", set())
234+
if len(set_axises) == 0:
235+
return None
236+
elif len(set_axises) == 1:
237+
axis, = set_axises
238+
return axis
239+
else:
240+
print(set_axises)
241+
raise RuntimeError(
242+
f"The 'axis' attribute of this {type(self).__name__} object "
243+
f"has been set multiple times, but a {type(self).__name__} "
244+
f"can only be used for one Axis at a time")
245+
246+
@axis.setter
247+
def axis(self, axis):
248+
if not hasattr(self, "_set_axises"):
249+
self._set_axises = set()
250+
self._set_axises.add(axis)
251+
252+
set_axis = axis.fset
225253

226254
def create_dummy_axis(self, **kwargs):
227255
if self.axis is None:

0 commit comments

Comments
 (0)