Skip to content

Commit 35a73a2

Browse files
committed
Enforce convert() and un_convert()
1 parent 39ba96b commit 35a73a2

File tree

3 files changed

+81
-8
lines changed

3 files changed

+81
-8
lines changed

lib/matplotlib/tests/test_axes.py

+20-5
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,10 @@ def test_matshow():
9797
])
9898
def test_formatter_ticker():
9999
import matplotlib.testing.jpl_units as units
100-
units.register()
100+
# Catch warnings thrown whilst jpl unit converters don't have an
101+
# un_convert() method
102+
with pytest.warns(Warning, match='does not define an un_convert'):
103+
units.register()
101104

102105
# This should affect the tick size. (Tests issue #543)
103106
matplotlib.rcParams['lines.markeredgewidth'] = 30
@@ -486,7 +489,10 @@ def test_polar_alignment():
486489
def test_fill_units():
487490
from datetime import datetime
488491
import matplotlib.testing.jpl_units as units
489-
units.register()
492+
# Catch warnings thrown whilst jpl unit converters don't have an
493+
# un_convert() method
494+
with pytest.warns(Warning, match='does not define an un_convert'):
495+
units.register()
490496

491497
# generate some data
492498
t = units.Epoch("ET", dt=datetime(2009, 4, 27))
@@ -654,7 +660,10 @@ def test_polar_wrap(fig_test, fig_ref):
654660
@check_figures_equal()
655661
def test_polar_units_1(fig_test, fig_ref):
656662
import matplotlib.testing.jpl_units as units
657-
units.register()
663+
# Catch warnings thrown whilst jpl unit converters don't have an
664+
# un_convert() method
665+
with pytest.warns(Warning, match='does not define an un_convert'):
666+
units.register()
658667
xs = [30.0, 45.0, 60.0, 90.0]
659668
ys = [1.0, 2.0, 3.0, 4.0]
660669

@@ -838,7 +847,10 @@ def test_aitoff_proj():
838847
def test_axvspan_epoch():
839848
from datetime import datetime
840849
import matplotlib.testing.jpl_units as units
841-
units.register()
850+
# Catch warnings thrown whilst jpl unit converters don't have an
851+
# un_convert() method
852+
with pytest.warns(Warning, match='does not define an un_convert'):
853+
units.register()
842854

843855
# generate some data
844856
t0 = units.Epoch("ET", dt=datetime(2009, 1, 20))
@@ -854,7 +866,10 @@ def test_axvspan_epoch():
854866
def test_axhspan_epoch():
855867
from datetime import datetime
856868
import matplotlib.testing.jpl_units as units
857-
units.register()
869+
# Catch warnings thrown whilst jpl unit converters don't have an
870+
# un_convert() method
871+
with pytest.warns(Warning, match='does not define an un_convert'):
872+
units.register()
858873

859874
# generate some data
860875
t0 = units.Epoch("ET", dt=datetime(2009, 1, 20))

lib/matplotlib/tests/test_units.py

+27-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from unittest.mock import MagicMock
44

55
import matplotlib.pyplot as plt
6+
from matplotlib import cbook
67
from matplotlib.testing.decorators import check_figures_equal, image_comparison
78
import matplotlib.units as munits
89
import numpy as np
@@ -56,6 +57,9 @@ def convert(value, unit, axis):
5657
else:
5758
return Quantity(value, axis.get_units()).to(unit).magnitude
5859

60+
def un_convert(value, unit, axis):
61+
return Quantitfy(value, unit)
62+
5963
def default_units(value, axis):
6064
if hasattr(value, 'units'):
6165
return value.units
@@ -68,6 +72,7 @@ def default_units(value, axis):
6872
qc.convert = MagicMock(side_effect=convert)
6973
qc.axisinfo = MagicMock(side_effect=lambda u, a: munits.AxisInfo(label=u))
7074
qc.default_units = MagicMock(side_effect=default_units)
75+
qc.un_convert = MagicMock(side_effect=un_convert)
7176
return qc
7277

7378

@@ -124,7 +129,10 @@ def test_empty_set_limits_with_units(quantity_converter):
124129
savefig_kwarg={'dpi': 120}, style='mpl20')
125130
def test_jpl_bar_units():
126131
import matplotlib.testing.jpl_units as units
127-
units.register()
132+
# Catch warnings thrown whilst jpl unit converters don't have an
133+
# un_convert() method
134+
with pytest.warns(Warning, match='does not define an un_convert'):
135+
units.register()
128136

129137
day = units.Duration("ET", 24.0 * 60.0 * 60.0)
130138
x = [0*units.km, 1*units.km, 2*units.km]
@@ -140,7 +148,10 @@ def test_jpl_bar_units():
140148
savefig_kwarg={'dpi': 120}, style='mpl20')
141149
def test_jpl_barh_units():
142150
import matplotlib.testing.jpl_units as units
143-
units.register()
151+
# Catch warnings thrown whilst jpl unit converters don't have an
152+
# un_convert() method
153+
with pytest.warns(Warning, match='does not define an un_convert'):
154+
units.register()
144155

145156
day = units.Duration("ET", 24.0 * 60.0 * 60.0)
146157
x = [0*units.km, 1*units.km, 2*units.km]
@@ -175,3 +186,17 @@ class subdate(datetime):
175186

176187
fig_test.subplots().plot(subdate(2000, 1, 1), 0, "o")
177188
fig_ref.subplots().plot(datetime(2000, 1, 1), 0, "o")
189+
190+
191+
def test_no_conveter_warnings():
192+
class Converter(munits.ConversionInterface):
193+
pass
194+
195+
# Check that a converter without a manuallly defined convert() method
196+
# warns
197+
with pytest.warns(cbook.deprecation.MatplotlibDeprecationWarning):
198+
Converter.convert(0, 0, 0)
199+
200+
# Check that manually defining a conveter doesn't warn
201+
Converter.convert = lambda obj, unit, axis: obj
202+
Converter.convert(0, 0, 0)

lib/matplotlib/units.py

+34-1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ def convert(value, unit, axis):
2222
'Convert a datetime value to a scalar or array'
2323
return dates.date2num(value)
2424
25+
@staticmethod
26+
def un_convert(value, unit, axis):
27+
'Convert a float back to a datetime value'
28+
return dates.num2date(value)
29+
2530
@staticmethod
2631
def axisinfo(unit, axis):
2732
'Return major and minor tick locators and formatters'
@@ -44,6 +49,7 @@ def default_units(x, axis):
4449

4550
from decimal import Decimal
4651
from numbers import Number
52+
import warnings
4753

4854
import numpy as np
4955
from numpy import ma
@@ -127,6 +133,7 @@ def default_units(x, axis):
127133
"""
128134
return None
129135

136+
# Make this an abstractmethod in 3.5
130137
@staticmethod
131138
def convert(obj, unit, axis):
132139
"""
@@ -135,15 +142,25 @@ def convert(obj, unit, axis):
135142
If *obj* is a sequence, return the converted sequence. The output must
136143
be a sequence of scalars that can be used by the numpy array layer.
137144
"""
145+
cbook.warn_deprecated(
146+
'3.3',
147+
message=('Using the default "does nothing" convert() method for '
148+
'Matplotlib ConversionInterface converters is deprecated '
149+
'and will raise an error in version 3.5. '
150+
'Please manually override convert().'))
138151
return obj
139152

153+
# Uncomment this in version 3.5 to enforce an un_convert() method
154+
'''
140155
@staticmethod
156+
@abc.abstractmethod
141157
def un_convert(data, unit, axis):
142158
"""
143159
Convert data that has already been converted back to its original
144160
value.
145161
"""
146-
return data
162+
pass
163+
'''
147164

148165
@staticmethod
149166
def is_numlike(x):
@@ -188,6 +205,14 @@ def convert(value, unit, axis):
188205
converter = ma.asarray
189206
return converter(value, dtype=np.float)
190207

208+
@staticmethod
209+
def un_convert(value, unit, axis):
210+
"""
211+
Un-convert from floats to Decimals.
212+
"""
213+
return Decimal(value)
214+
215+
191216
@staticmethod
192217
def axisinfo(unit, axis):
193218
# Since Decimal is a kind of Number, don't need specific axisinfo.
@@ -202,6 +227,14 @@ def default_units(x, axis):
202227
class Registry(dict):
203228
"""Register types with conversion interface."""
204229

230+
def __setitem__(self, cls, converter):
231+
if not hasattr(converter, 'un_convert'):
232+
warnings.warn(
233+
f'{converter.__class__.__name__} does not define an '
234+
'un_convert() method. From Matplotlib 3.5 this will be '
235+
'required, and if not present will raise an error.')
236+
super().__setitem__(cls, converter)
237+
205238
def get_converter(self, x):
206239
"""Get the converter interface instance for *x*, or None."""
207240
if hasattr(x, "values"):

0 commit comments

Comments
 (0)