test_preprocess_data.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. import re
  2. import sys
  3. import numpy as np
  4. import pytest
  5. from matplotlib import _preprocess_data
  6. from matplotlib.axes import Axes
  7. from matplotlib.testing import subprocess_run_for_testing
  8. from matplotlib.testing.decorators import check_figures_equal
  9. # Notes on testing the plotting functions itself
  10. # * the individual decorated plotting functions are tested in 'test_axes.py'
  11. # * that pyplot functions accept a data kwarg is only tested in
  12. # test_axes.test_pie_linewidth_0
  13. # this gets used in multiple tests, so define it here
  14. @_preprocess_data(replace_names=["x", "y"], label_namer="y")
  15. def plot_func(ax, x, y, ls="x", label=None, w="xyz"):
  16. return f"x: {list(x)}, y: {list(y)}, ls: {ls}, w: {w}, label: {label}"
  17. all_funcs = [plot_func]
  18. all_func_ids = ['plot_func']
  19. def test_compiletime_checks():
  20. """Test decorator invocations -> no replacements."""
  21. def func(ax, x, y): pass
  22. def func_args(ax, x, y, *args): pass
  23. def func_kwargs(ax, x, y, **kwargs): pass
  24. def func_no_ax_args(*args, **kwargs): pass
  25. # this is ok
  26. _preprocess_data(replace_names=["x", "y"])(func)
  27. _preprocess_data(replace_names=["x", "y"])(func_kwargs)
  28. # this has "enough" information to do all the replaces
  29. _preprocess_data(replace_names=["x", "y"])(func_args)
  30. # no positional_parameter_names but needed due to replaces
  31. with pytest.raises(AssertionError):
  32. # z is unknown
  33. _preprocess_data(replace_names=["x", "y", "z"])(func_args)
  34. # no replacements at all -> all ok...
  35. _preprocess_data(replace_names=[], label_namer=None)(func)
  36. _preprocess_data(replace_names=[], label_namer=None)(func_args)
  37. _preprocess_data(replace_names=[], label_namer=None)(func_kwargs)
  38. _preprocess_data(replace_names=[], label_namer=None)(func_no_ax_args)
  39. # label namer is unknown
  40. with pytest.raises(AssertionError):
  41. _preprocess_data(label_namer="z")(func)
  42. with pytest.raises(AssertionError):
  43. _preprocess_data(label_namer="z")(func_args)
  44. @pytest.mark.parametrize('func', all_funcs, ids=all_func_ids)
  45. def test_function_call_without_data(func):
  46. """Test without data -> no replacements."""
  47. assert (func(None, "x", "y") ==
  48. "x: ['x'], y: ['y'], ls: x, w: xyz, label: None")
  49. assert (func(None, x="x", y="y") ==
  50. "x: ['x'], y: ['y'], ls: x, w: xyz, label: None")
  51. assert (func(None, "x", "y", label="") ==
  52. "x: ['x'], y: ['y'], ls: x, w: xyz, label: ")
  53. assert (func(None, "x", "y", label="text") ==
  54. "x: ['x'], y: ['y'], ls: x, w: xyz, label: text")
  55. assert (func(None, x="x", y="y", label="") ==
  56. "x: ['x'], y: ['y'], ls: x, w: xyz, label: ")
  57. assert (func(None, x="x", y="y", label="text") ==
  58. "x: ['x'], y: ['y'], ls: x, w: xyz, label: text")
  59. @pytest.mark.parametrize('func', all_funcs, ids=all_func_ids)
  60. def test_function_call_with_dict_input(func):
  61. """Tests with dict input, unpacking via preprocess_pipeline"""
  62. data = {'a': 1, 'b': 2}
  63. assert (func(None, data.keys(), data.values()) ==
  64. "x: ['a', 'b'], y: [1, 2], ls: x, w: xyz, label: None")
  65. @pytest.mark.parametrize('func', all_funcs, ids=all_func_ids)
  66. def test_function_call_with_dict_data(func):
  67. """Test with dict data -> label comes from the value of 'x' parameter."""
  68. data = {"a": [1, 2], "b": [8, 9], "w": "NOT"}
  69. assert (func(None, "a", "b", data=data) ==
  70. "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: b")
  71. assert (func(None, x="a", y="b", data=data) ==
  72. "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: b")
  73. assert (func(None, "a", "b", label="", data=data) ==
  74. "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: ")
  75. assert (func(None, "a", "b", label="text", data=data) ==
  76. "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: text")
  77. assert (func(None, x="a", y="b", label="", data=data) ==
  78. "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: ")
  79. assert (func(None, x="a", y="b", label="text", data=data) ==
  80. "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: text")
  81. @pytest.mark.parametrize('func', all_funcs, ids=all_func_ids)
  82. def test_function_call_with_dict_data_not_in_data(func):
  83. """Test the case that one var is not in data -> half replaces, half kept"""
  84. data = {"a": [1, 2], "w": "NOT"}
  85. assert (func(None, "a", "b", data=data) ==
  86. "x: [1, 2], y: ['b'], ls: x, w: xyz, label: b")
  87. assert (func(None, x="a", y="b", data=data) ==
  88. "x: [1, 2], y: ['b'], ls: x, w: xyz, label: b")
  89. assert (func(None, "a", "b", label="", data=data) ==
  90. "x: [1, 2], y: ['b'], ls: x, w: xyz, label: ")
  91. assert (func(None, "a", "b", label="text", data=data) ==
  92. "x: [1, 2], y: ['b'], ls: x, w: xyz, label: text")
  93. assert (func(None, x="a", y="b", label="", data=data) ==
  94. "x: [1, 2], y: ['b'], ls: x, w: xyz, label: ")
  95. assert (func(None, x="a", y="b", label="text", data=data) ==
  96. "x: [1, 2], y: ['b'], ls: x, w: xyz, label: text")
  97. @pytest.mark.parametrize('func', all_funcs, ids=all_func_ids)
  98. def test_function_call_with_pandas_data(func, pd):
  99. """Test with pandas dataframe -> label comes from ``data["col"].name``."""
  100. data = pd.DataFrame({"a": np.array([1, 2], dtype=np.int32),
  101. "b": np.array([8, 9], dtype=np.int32),
  102. "w": ["NOT", "NOT"]})
  103. assert (func(None, "a", "b", data=data) ==
  104. "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: b")
  105. assert (func(None, x="a", y="b", data=data) ==
  106. "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: b")
  107. assert (func(None, "a", "b", label="", data=data) ==
  108. "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: ")
  109. assert (func(None, "a", "b", label="text", data=data) ==
  110. "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: text")
  111. assert (func(None, x="a", y="b", label="", data=data) ==
  112. "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: ")
  113. assert (func(None, x="a", y="b", label="text", data=data) ==
  114. "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: text")
  115. def test_function_call_replace_all():
  116. """Test without a "replace_names" argument, all vars should be replaced."""
  117. data = {"a": [1, 2], "b": [8, 9], "x": "xyz"}
  118. @_preprocess_data(label_namer="y")
  119. def func_replace_all(ax, x, y, ls="x", label=None, w="NOT"):
  120. return f"x: {list(x)}, y: {list(y)}, ls: {ls}, w: {w}, label: {label}"
  121. assert (func_replace_all(None, "a", "b", w="x", data=data) ==
  122. "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: b")
  123. assert (func_replace_all(None, x="a", y="b", w="x", data=data) ==
  124. "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: b")
  125. assert (func_replace_all(None, "a", "b", w="x", label="", data=data) ==
  126. "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: ")
  127. assert (
  128. func_replace_all(None, "a", "b", w="x", label="text", data=data) ==
  129. "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: text")
  130. assert (
  131. func_replace_all(None, x="a", y="b", w="x", label="", data=data) ==
  132. "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: ")
  133. assert (
  134. func_replace_all(None, x="a", y="b", w="x", label="text", data=data) ==
  135. "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: text")
  136. def test_no_label_replacements():
  137. """Test with "label_namer=None" -> no label replacement at all."""
  138. @_preprocess_data(replace_names=["x", "y"], label_namer=None)
  139. def func_no_label(ax, x, y, ls="x", label=None, w="xyz"):
  140. return f"x: {list(x)}, y: {list(y)}, ls: {ls}, w: {w}, label: {label}"
  141. data = {"a": [1, 2], "b": [8, 9], "w": "NOT"}
  142. assert (func_no_label(None, "a", "b", data=data) ==
  143. "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: None")
  144. assert (func_no_label(None, x="a", y="b", data=data) ==
  145. "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: None")
  146. assert (func_no_label(None, "a", "b", label="", data=data) ==
  147. "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: ")
  148. assert (func_no_label(None, "a", "b", label="text", data=data) ==
  149. "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: text")
  150. def test_more_args_than_pos_parameter():
  151. @_preprocess_data(replace_names=["x", "y"], label_namer="y")
  152. def func(ax, x, y, z=1):
  153. pass
  154. data = {"a": [1, 2], "b": [8, 9], "w": "NOT"}
  155. with pytest.raises(TypeError):
  156. func(None, "a", "b", "z", "z", data=data)
  157. def test_docstring_addition():
  158. @_preprocess_data()
  159. def funcy(ax, *args, **kwargs):
  160. """
  161. Parameters
  162. ----------
  163. data : indexable object, optional
  164. DATA_PARAMETER_PLACEHOLDER
  165. """
  166. assert re.search(r"all parameters also accept a string", funcy.__doc__)
  167. assert not re.search(r"the following parameters", funcy.__doc__)
  168. @_preprocess_data(replace_names=[])
  169. def funcy(ax, x, y, z, bar=None):
  170. """
  171. Parameters
  172. ----------
  173. data : indexable object, optional
  174. DATA_PARAMETER_PLACEHOLDER
  175. """
  176. assert not re.search(r"all parameters also accept a string", funcy.__doc__)
  177. assert not re.search(r"the following parameters", funcy.__doc__)
  178. @_preprocess_data(replace_names=["bar"])
  179. def funcy(ax, x, y, z, bar=None):
  180. """
  181. Parameters
  182. ----------
  183. data : indexable object, optional
  184. DATA_PARAMETER_PLACEHOLDER
  185. """
  186. assert not re.search(r"all parameters also accept a string", funcy.__doc__)
  187. assert not re.search(r"the following parameters .*: \*bar\*\.",
  188. funcy.__doc__)
  189. @_preprocess_data(replace_names=["x", "t"])
  190. def funcy(ax, x, y, z, t=None):
  191. """
  192. Parameters
  193. ----------
  194. data : indexable object, optional
  195. DATA_PARAMETER_PLACEHOLDER
  196. """
  197. assert not re.search(r"all parameters also accept a string", funcy.__doc__)
  198. assert not re.search(r"the following parameters .*: \*x\*, \*t\*\.",
  199. funcy.__doc__)
  200. def test_data_parameter_replacement():
  201. """
  202. Test that the docstring contains the correct *data* parameter stub
  203. for all methods that we run _preprocess_data() on.
  204. """
  205. program = (
  206. "import logging; "
  207. "logging.basicConfig(level=logging.DEBUG); "
  208. "import matplotlib.pyplot as plt"
  209. )
  210. cmd = [sys.executable, "-c", program]
  211. completed_proc = subprocess_run_for_testing(
  212. cmd, text=True, capture_output=True
  213. )
  214. assert 'data parameter docstring error' not in completed_proc.stderr
  215. class TestPlotTypes:
  216. plotters = [Axes.scatter, Axes.bar, Axes.plot]
  217. @pytest.mark.parametrize('plotter', plotters)
  218. @check_figures_equal(extensions=['png'])
  219. def test_dict_unpack(self, plotter, fig_test, fig_ref):
  220. x = [1, 2, 3]
  221. y = [4, 5, 6]
  222. ddict = dict(zip(x, y))
  223. plotter(fig_test.subplots(),
  224. ddict.keys(), ddict.values())
  225. plotter(fig_ref.subplots(), x, y)
  226. @pytest.mark.parametrize('plotter', plotters)
  227. @check_figures_equal(extensions=['png'])
  228. def test_data_kwarg(self, plotter, fig_test, fig_ref):
  229. x = [1, 2, 3]
  230. y = [4, 5, 6]
  231. plotter(fig_test.subplots(), 'xval', 'yval',
  232. data={'xval': x, 'yval': y})
  233. plotter(fig_ref.subplots(), x, y)