test_cbook.py 33 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049
  1. from __future__ import annotations
  2. import itertools
  3. import pathlib
  4. import pickle
  5. import sys
  6. from typing import Any
  7. from unittest.mock import patch, Mock
  8. from datetime import datetime, date, timedelta
  9. import numpy as np
  10. from numpy.testing import (assert_array_equal, assert_approx_equal,
  11. assert_array_almost_equal)
  12. import pytest
  13. from matplotlib import _api, cbook
  14. import matplotlib.colors as mcolors
  15. from matplotlib.cbook import delete_masked_points, strip_math
  16. from types import ModuleType
  17. class Test_delete_masked_points:
  18. def test_bad_first_arg(self):
  19. with pytest.raises(ValueError):
  20. delete_masked_points('a string', np.arange(1.0, 7.0))
  21. def test_string_seq(self):
  22. a1 = ['a', 'b', 'c', 'd', 'e', 'f']
  23. a2 = [1, 2, 3, np.nan, np.nan, 6]
  24. result1, result2 = delete_masked_points(a1, a2)
  25. ind = [0, 1, 2, 5]
  26. assert_array_equal(result1, np.array(a1)[ind])
  27. assert_array_equal(result2, np.array(a2)[ind])
  28. def test_datetime(self):
  29. dates = [datetime(2008, 1, 1), datetime(2008, 1, 2),
  30. datetime(2008, 1, 3), datetime(2008, 1, 4),
  31. datetime(2008, 1, 5), datetime(2008, 1, 6)]
  32. a_masked = np.ma.array([1, 2, 3, np.nan, np.nan, 6],
  33. mask=[False, False, True, True, False, False])
  34. actual = delete_masked_points(dates, a_masked)
  35. ind = [0, 1, 5]
  36. assert_array_equal(actual[0], np.array(dates)[ind])
  37. assert_array_equal(actual[1], a_masked[ind].compressed())
  38. def test_rgba(self):
  39. a_masked = np.ma.array([1, 2, 3, np.nan, np.nan, 6],
  40. mask=[False, False, True, True, False, False])
  41. a_rgba = mcolors.to_rgba_array(['r', 'g', 'b', 'c', 'm', 'y'])
  42. actual = delete_masked_points(a_masked, a_rgba)
  43. ind = [0, 1, 5]
  44. assert_array_equal(actual[0], a_masked[ind].compressed())
  45. assert_array_equal(actual[1], a_rgba[ind])
  46. class Test_boxplot_stats:
  47. def setup_method(self):
  48. np.random.seed(937)
  49. self.nrows = 37
  50. self.ncols = 4
  51. self.data = np.random.lognormal(size=(self.nrows, self.ncols),
  52. mean=1.5, sigma=1.75)
  53. self.known_keys = sorted([
  54. 'mean', 'med', 'q1', 'q3', 'iqr',
  55. 'cilo', 'cihi', 'whislo', 'whishi',
  56. 'fliers', 'label'
  57. ])
  58. self.std_results = cbook.boxplot_stats(self.data)
  59. self.known_nonbootstrapped_res = {
  60. 'cihi': 6.8161283264444847,
  61. 'cilo': -0.1489815330368689,
  62. 'iqr': 13.492709959447094,
  63. 'mean': 13.00447442387868,
  64. 'med': 3.3335733967038079,
  65. 'fliers': np.array([
  66. 92.55467075, 87.03819018, 42.23204914, 39.29390996
  67. ]),
  68. 'q1': 1.3597529879465153,
  69. 'q3': 14.85246294739361,
  70. 'whishi': 27.899688243699629,
  71. 'whislo': 0.042143774965502923
  72. }
  73. self.known_bootstrapped_ci = {
  74. 'cihi': 8.939577523357828,
  75. 'cilo': 1.8692703958676578,
  76. }
  77. self.known_whis3_res = {
  78. 'whishi': 42.232049135969874,
  79. 'whislo': 0.042143774965502923,
  80. 'fliers': np.array([92.55467075, 87.03819018]),
  81. }
  82. self.known_res_percentiles = {
  83. 'whislo': 0.1933685896907924,
  84. 'whishi': 42.232049135969874
  85. }
  86. self.known_res_range = {
  87. 'whislo': 0.042143774965502923,
  88. 'whishi': 92.554670752188699
  89. }
  90. def test_form_main_list(self):
  91. assert isinstance(self.std_results, list)
  92. def test_form_each_dict(self):
  93. for res in self.std_results:
  94. assert isinstance(res, dict)
  95. def test_form_dict_keys(self):
  96. for res in self.std_results:
  97. assert set(res) <= set(self.known_keys)
  98. def test_results_baseline(self):
  99. res = self.std_results[0]
  100. for key, value in self.known_nonbootstrapped_res.items():
  101. assert_array_almost_equal(res[key], value)
  102. def test_results_bootstrapped(self):
  103. results = cbook.boxplot_stats(self.data, bootstrap=10000)
  104. res = results[0]
  105. for key, value in self.known_bootstrapped_ci.items():
  106. assert_approx_equal(res[key], value)
  107. def test_results_whiskers_float(self):
  108. results = cbook.boxplot_stats(self.data, whis=3)
  109. res = results[0]
  110. for key, value in self.known_whis3_res.items():
  111. assert_array_almost_equal(res[key], value)
  112. def test_results_whiskers_range(self):
  113. results = cbook.boxplot_stats(self.data, whis=[0, 100])
  114. res = results[0]
  115. for key, value in self.known_res_range.items():
  116. assert_array_almost_equal(res[key], value)
  117. def test_results_whiskers_percentiles(self):
  118. results = cbook.boxplot_stats(self.data, whis=[5, 95])
  119. res = results[0]
  120. for key, value in self.known_res_percentiles.items():
  121. assert_array_almost_equal(res[key], value)
  122. def test_results_withlabels(self):
  123. labels = ['Test1', 2, 'Aardvark', 4]
  124. results = cbook.boxplot_stats(self.data, labels=labels)
  125. for lab, res in zip(labels, results):
  126. assert res['label'] == lab
  127. results = cbook.boxplot_stats(self.data)
  128. for res in results:
  129. assert 'label' not in res
  130. def test_label_error(self):
  131. labels = [1, 2]
  132. with pytest.raises(ValueError):
  133. cbook.boxplot_stats(self.data, labels=labels)
  134. def test_bad_dims(self):
  135. data = np.random.normal(size=(34, 34, 34))
  136. with pytest.raises(ValueError):
  137. cbook.boxplot_stats(data)
  138. def test_boxplot_stats_autorange_false(self):
  139. x = np.zeros(shape=140)
  140. x = np.hstack([-25, x, 25])
  141. bstats_false = cbook.boxplot_stats(x, autorange=False)
  142. bstats_true = cbook.boxplot_stats(x, autorange=True)
  143. assert bstats_false[0]['whislo'] == 0
  144. assert bstats_false[0]['whishi'] == 0
  145. assert_array_almost_equal(bstats_false[0]['fliers'], [-25, 25])
  146. assert bstats_true[0]['whislo'] == -25
  147. assert bstats_true[0]['whishi'] == 25
  148. assert_array_almost_equal(bstats_true[0]['fliers'], [])
  149. class Hashable:
  150. def dummy(self): pass
  151. class Unhashable:
  152. __hash__ = None # type: ignore[assignment]
  153. def dummy(self): pass
  154. class Test_callback_registry:
  155. def setup_method(self):
  156. self.signal = 'test'
  157. self.callbacks = cbook.CallbackRegistry()
  158. def connect(self, s, func, pickle):
  159. if pickle:
  160. return self.callbacks.connect(s, func)
  161. else:
  162. return self.callbacks._connect_picklable(s, func)
  163. def disconnect(self, cid):
  164. return self.callbacks.disconnect(cid)
  165. def count(self):
  166. count1 = sum(s == self.signal for s, p in self.callbacks._func_cid_map)
  167. count2 = len(self.callbacks.callbacks.get(self.signal))
  168. assert count1 == count2
  169. return count1
  170. def is_empty(self):
  171. np.testing.break_cycles()
  172. assert [*self.callbacks._func_cid_map] == []
  173. assert self.callbacks.callbacks == {}
  174. assert self.callbacks._pickled_cids == set()
  175. def is_not_empty(self):
  176. np.testing.break_cycles()
  177. assert [*self.callbacks._func_cid_map] != []
  178. assert self.callbacks.callbacks != {}
  179. def test_cid_restore(self):
  180. cb = cbook.CallbackRegistry()
  181. cb.connect('a', lambda: None)
  182. cb2 = pickle.loads(pickle.dumps(cb))
  183. cid = cb2.connect('c', lambda: None)
  184. assert cid == 1
  185. @pytest.mark.parametrize('pickle', [True, False])
  186. @pytest.mark.parametrize('cls', [Hashable, Unhashable])
  187. def test_callback_complete(self, pickle, cls):
  188. # ensure we start with an empty registry
  189. self.is_empty()
  190. # create a class for testing
  191. mini_me = cls()
  192. # test that we can add a callback
  193. cid1 = self.connect(self.signal, mini_me.dummy, pickle)
  194. assert type(cid1) is int
  195. self.is_not_empty()
  196. # test that we don't add a second callback
  197. cid2 = self.connect(self.signal, mini_me.dummy, pickle)
  198. assert cid1 == cid2
  199. self.is_not_empty()
  200. assert len([*self.callbacks._func_cid_map]) == 1
  201. assert len(self.callbacks.callbacks) == 1
  202. del mini_me
  203. # check we now have no callbacks registered
  204. self.is_empty()
  205. @pytest.mark.parametrize('pickle', [True, False])
  206. @pytest.mark.parametrize('cls', [Hashable, Unhashable])
  207. def test_callback_disconnect(self, pickle, cls):
  208. # ensure we start with an empty registry
  209. self.is_empty()
  210. # create a class for testing
  211. mini_me = cls()
  212. # test that we can add a callback
  213. cid1 = self.connect(self.signal, mini_me.dummy, pickle)
  214. assert type(cid1) is int
  215. self.is_not_empty()
  216. self.disconnect(cid1)
  217. # check we now have no callbacks registered
  218. self.is_empty()
  219. @pytest.mark.parametrize('pickle', [True, False])
  220. @pytest.mark.parametrize('cls', [Hashable, Unhashable])
  221. def test_callback_wrong_disconnect(self, pickle, cls):
  222. # ensure we start with an empty registry
  223. self.is_empty()
  224. # create a class for testing
  225. mini_me = cls()
  226. # test that we can add a callback
  227. cid1 = self.connect(self.signal, mini_me.dummy, pickle)
  228. assert type(cid1) is int
  229. self.is_not_empty()
  230. self.disconnect("foo")
  231. # check we still have callbacks registered
  232. self.is_not_empty()
  233. @pytest.mark.parametrize('pickle', [True, False])
  234. @pytest.mark.parametrize('cls', [Hashable, Unhashable])
  235. def test_registration_on_non_empty_registry(self, pickle, cls):
  236. # ensure we start with an empty registry
  237. self.is_empty()
  238. # setup the registry with a callback
  239. mini_me = cls()
  240. self.connect(self.signal, mini_me.dummy, pickle)
  241. # Add another callback
  242. mini_me2 = cls()
  243. self.connect(self.signal, mini_me2.dummy, pickle)
  244. # Remove and add the second callback
  245. mini_me2 = cls()
  246. self.connect(self.signal, mini_me2.dummy, pickle)
  247. # We still have 2 references
  248. self.is_not_empty()
  249. assert self.count() == 2
  250. # Removing the last 2 references
  251. mini_me = None
  252. mini_me2 = None
  253. self.is_empty()
  254. def test_pickling(self):
  255. assert hasattr(pickle.loads(pickle.dumps(cbook.CallbackRegistry())),
  256. "callbacks")
  257. def test_callbackregistry_default_exception_handler(capsys, monkeypatch):
  258. cb = cbook.CallbackRegistry()
  259. cb.connect("foo", lambda: None)
  260. monkeypatch.setattr(
  261. cbook, "_get_running_interactive_framework", lambda: None)
  262. with pytest.raises(TypeError):
  263. cb.process("foo", "argument mismatch")
  264. outerr = capsys.readouterr()
  265. assert outerr.out == outerr.err == ""
  266. monkeypatch.setattr(
  267. cbook, "_get_running_interactive_framework", lambda: "not-none")
  268. cb.process("foo", "argument mismatch") # No error in that case.
  269. outerr = capsys.readouterr()
  270. assert outerr.out == ""
  271. assert "takes 0 positional arguments but 1 was given" in outerr.err
  272. def raising_cb_reg(func):
  273. class TestException(Exception):
  274. pass
  275. def raise_runtime_error():
  276. raise RuntimeError
  277. def raise_value_error():
  278. raise ValueError
  279. def transformer(excp):
  280. if isinstance(excp, RuntimeError):
  281. raise TestException
  282. raise excp
  283. # old default
  284. cb_old = cbook.CallbackRegistry(exception_handler=None)
  285. cb_old.connect('foo', raise_runtime_error)
  286. # filter
  287. cb_filt = cbook.CallbackRegistry(exception_handler=transformer)
  288. cb_filt.connect('foo', raise_runtime_error)
  289. # filter
  290. cb_filt_pass = cbook.CallbackRegistry(exception_handler=transformer)
  291. cb_filt_pass.connect('foo', raise_value_error)
  292. return pytest.mark.parametrize('cb, excp',
  293. [[cb_old, RuntimeError],
  294. [cb_filt, TestException],
  295. [cb_filt_pass, ValueError]])(func)
  296. @raising_cb_reg
  297. def test_callbackregistry_custom_exception_handler(monkeypatch, cb, excp):
  298. monkeypatch.setattr(
  299. cbook, "_get_running_interactive_framework", lambda: None)
  300. with pytest.raises(excp):
  301. cb.process('foo')
  302. def test_callbackregistry_signals():
  303. cr = cbook.CallbackRegistry(signals=["foo"])
  304. results = []
  305. def cb(x): results.append(x)
  306. cr.connect("foo", cb)
  307. with pytest.raises(ValueError):
  308. cr.connect("bar", cb)
  309. cr.process("foo", 1)
  310. with pytest.raises(ValueError):
  311. cr.process("bar", 1)
  312. assert results == [1]
  313. def test_callbackregistry_blocking():
  314. # Needs an exception handler for interactive testing environments
  315. # that would only print this out instead of raising the exception
  316. def raise_handler(excp):
  317. raise excp
  318. cb = cbook.CallbackRegistry(exception_handler=raise_handler)
  319. def test_func1():
  320. raise ValueError("1 should be blocked")
  321. def test_func2():
  322. raise ValueError("2 should be blocked")
  323. cb.connect("test1", test_func1)
  324. cb.connect("test2", test_func2)
  325. # block all of the callbacks to make sure they aren't processed
  326. with cb.blocked():
  327. cb.process("test1")
  328. cb.process("test2")
  329. # block individual callbacks to make sure the other is still processed
  330. with cb.blocked(signal="test1"):
  331. # Blocked
  332. cb.process("test1")
  333. # Should raise
  334. with pytest.raises(ValueError, match="2 should be blocked"):
  335. cb.process("test2")
  336. # Make sure the original callback functions are there after blocking
  337. with pytest.raises(ValueError, match="1 should be blocked"):
  338. cb.process("test1")
  339. with pytest.raises(ValueError, match="2 should be blocked"):
  340. cb.process("test2")
  341. @pytest.mark.parametrize('line, result', [
  342. ('a : no_comment', 'a : no_comment'),
  343. ('a : "quoted str"', 'a : "quoted str"'),
  344. ('a : "quoted str" # comment', 'a : "quoted str"'),
  345. ('a : "#000000"', 'a : "#000000"'),
  346. ('a : "#000000" # comment', 'a : "#000000"'),
  347. ('a : ["#000000", "#FFFFFF"]', 'a : ["#000000", "#FFFFFF"]'),
  348. ('a : ["#000000", "#FFFFFF"] # comment', 'a : ["#000000", "#FFFFFF"]'),
  349. ('a : val # a comment "with quotes"', 'a : val'),
  350. ('# only comment "with quotes" xx', ''),
  351. ])
  352. def test_strip_comment(line, result):
  353. """Strip everything from the first unquoted #."""
  354. assert cbook._strip_comment(line) == result
  355. def test_strip_comment_invalid():
  356. with pytest.raises(ValueError, match="Missing closing quote"):
  357. cbook._strip_comment('grid.color: "aa')
  358. def test_sanitize_sequence():
  359. d = {'a': 1, 'b': 2, 'c': 3}
  360. k = ['a', 'b', 'c']
  361. v = [1, 2, 3]
  362. i = [('a', 1), ('b', 2), ('c', 3)]
  363. assert k == sorted(cbook.sanitize_sequence(d.keys()))
  364. assert v == sorted(cbook.sanitize_sequence(d.values()))
  365. assert i == sorted(cbook.sanitize_sequence(d.items()))
  366. assert i == cbook.sanitize_sequence(i)
  367. assert k == cbook.sanitize_sequence(k)
  368. fail_mapping: tuple[tuple[dict, dict], ...] = (
  369. ({'a': 1, 'b': 2}, {'alias_mapping': {'a': ['b']}}),
  370. ({'a': 1, 'b': 2}, {'alias_mapping': {'a': ['a', 'b']}}),
  371. )
  372. pass_mapping: tuple[tuple[Any, dict, dict], ...] = (
  373. (None, {}, {}),
  374. ({'a': 1, 'b': 2}, {'a': 1, 'b': 2}, {}),
  375. ({'b': 2}, {'a': 2}, {'alias_mapping': {'a': ['a', 'b']}}),
  376. )
  377. @pytest.mark.parametrize('inp, kwargs_to_norm', fail_mapping)
  378. def test_normalize_kwargs_fail(inp, kwargs_to_norm):
  379. with pytest.raises(TypeError), _api.suppress_matplotlib_deprecation_warning():
  380. cbook.normalize_kwargs(inp, **kwargs_to_norm)
  381. @pytest.mark.parametrize('inp, expected, kwargs_to_norm',
  382. pass_mapping)
  383. def test_normalize_kwargs_pass(inp, expected, kwargs_to_norm):
  384. with _api.suppress_matplotlib_deprecation_warning():
  385. # No other warning should be emitted.
  386. assert expected == cbook.normalize_kwargs(inp, **kwargs_to_norm)
  387. def test_warn_external(recwarn):
  388. _api.warn_external("oops")
  389. assert len(recwarn) == 1
  390. if sys.version_info[:2] >= (3, 12):
  391. # With Python 3.12, we let Python figure out the stacklevel using the
  392. # `skip_file_prefixes` argument, which cannot exempt tests, so just confirm
  393. # the filename is not in the package.
  394. basedir = pathlib.Path(__file__).parents[2]
  395. assert not recwarn[0].filename.startswith((str(basedir / 'matplotlib'),
  396. str(basedir / 'mpl_toolkits')))
  397. else:
  398. # On older Python versions, we manually calculated the stacklevel, and had an
  399. # exception for our own tests.
  400. assert recwarn[0].filename == __file__
  401. def test_warn_external_frame_embedded_python():
  402. with patch.object(cbook, "sys") as mock_sys:
  403. mock_sys._getframe = Mock(return_value=None)
  404. with pytest.warns(UserWarning, match=r"\Adummy\Z"):
  405. _api.warn_external("dummy")
  406. def test_to_prestep():
  407. x = np.arange(4)
  408. y1 = np.arange(4)
  409. y2 = np.arange(4)[::-1]
  410. xs, y1s, y2s = cbook.pts_to_prestep(x, y1, y2)
  411. x_target = np.asarray([0, 0, 1, 1, 2, 2, 3], dtype=float)
  412. y1_target = np.asarray([0, 1, 1, 2, 2, 3, 3], dtype=float)
  413. y2_target = np.asarray([3, 2, 2, 1, 1, 0, 0], dtype=float)
  414. assert_array_equal(x_target, xs)
  415. assert_array_equal(y1_target, y1s)
  416. assert_array_equal(y2_target, y2s)
  417. xs, y1s = cbook.pts_to_prestep(x, y1)
  418. assert_array_equal(x_target, xs)
  419. assert_array_equal(y1_target, y1s)
  420. def test_to_prestep_empty():
  421. steps = cbook.pts_to_prestep([], [])
  422. assert steps.shape == (2, 0)
  423. def test_to_poststep():
  424. x = np.arange(4)
  425. y1 = np.arange(4)
  426. y2 = np.arange(4)[::-1]
  427. xs, y1s, y2s = cbook.pts_to_poststep(x, y1, y2)
  428. x_target = np.asarray([0, 1, 1, 2, 2, 3, 3], dtype=float)
  429. y1_target = np.asarray([0, 0, 1, 1, 2, 2, 3], dtype=float)
  430. y2_target = np.asarray([3, 3, 2, 2, 1, 1, 0], dtype=float)
  431. assert_array_equal(x_target, xs)
  432. assert_array_equal(y1_target, y1s)
  433. assert_array_equal(y2_target, y2s)
  434. xs, y1s = cbook.pts_to_poststep(x, y1)
  435. assert_array_equal(x_target, xs)
  436. assert_array_equal(y1_target, y1s)
  437. def test_to_poststep_empty():
  438. steps = cbook.pts_to_poststep([], [])
  439. assert steps.shape == (2, 0)
  440. def test_to_midstep():
  441. x = np.arange(4)
  442. y1 = np.arange(4)
  443. y2 = np.arange(4)[::-1]
  444. xs, y1s, y2s = cbook.pts_to_midstep(x, y1, y2)
  445. x_target = np.asarray([0, .5, .5, 1.5, 1.5, 2.5, 2.5, 3], dtype=float)
  446. y1_target = np.asarray([0, 0, 1, 1, 2, 2, 3, 3], dtype=float)
  447. y2_target = np.asarray([3, 3, 2, 2, 1, 1, 0, 0], dtype=float)
  448. assert_array_equal(x_target, xs)
  449. assert_array_equal(y1_target, y1s)
  450. assert_array_equal(y2_target, y2s)
  451. xs, y1s = cbook.pts_to_midstep(x, y1)
  452. assert_array_equal(x_target, xs)
  453. assert_array_equal(y1_target, y1s)
  454. def test_to_midstep_empty():
  455. steps = cbook.pts_to_midstep([], [])
  456. assert steps.shape == (2, 0)
  457. @pytest.mark.parametrize(
  458. "args",
  459. [(np.arange(12).reshape(3, 4), 'a'),
  460. (np.arange(12), 'a'),
  461. (np.arange(12), np.arange(3))])
  462. def test_step_fails(args):
  463. with pytest.raises(ValueError):
  464. cbook.pts_to_prestep(*args)
  465. def test_grouper():
  466. class Dummy:
  467. pass
  468. a, b, c, d, e = objs = [Dummy() for _ in range(5)]
  469. g = cbook.Grouper()
  470. g.join(*objs)
  471. assert set(list(g)[0]) == set(objs)
  472. assert set(g.get_siblings(a)) == set(objs)
  473. for other in objs[1:]:
  474. assert g.joined(a, other)
  475. g.remove(a)
  476. for other in objs[1:]:
  477. assert not g.joined(a, other)
  478. for A, B in itertools.product(objs[1:], objs[1:]):
  479. assert g.joined(A, B)
  480. def test_grouper_private():
  481. class Dummy:
  482. pass
  483. objs = [Dummy() for _ in range(5)]
  484. g = cbook.Grouper()
  485. g.join(*objs)
  486. # reach in and touch the internals !
  487. mapping = g._mapping
  488. for o in objs:
  489. assert o in mapping
  490. base_set = mapping[objs[0]]
  491. for o in objs[1:]:
  492. assert mapping[o] is base_set
  493. def test_flatiter():
  494. x = np.arange(5)
  495. it = x.flat
  496. assert 0 == next(it)
  497. assert 1 == next(it)
  498. ret = cbook._safe_first_finite(it)
  499. assert ret == 0
  500. assert 0 == next(it)
  501. assert 1 == next(it)
  502. def test__safe_first_finite_all_nan():
  503. arr = np.full(2, np.nan)
  504. ret = cbook._safe_first_finite(arr)
  505. assert np.isnan(ret)
  506. def test__safe_first_finite_all_inf():
  507. arr = np.full(2, np.inf)
  508. ret = cbook._safe_first_finite(arr)
  509. assert np.isinf(ret)
  510. def test_reshape2d():
  511. class Dummy:
  512. pass
  513. xnew = cbook._reshape_2D([], 'x')
  514. assert np.shape(xnew) == (1, 0)
  515. x = [Dummy() for _ in range(5)]
  516. xnew = cbook._reshape_2D(x, 'x')
  517. assert np.shape(xnew) == (1, 5)
  518. x = np.arange(5)
  519. xnew = cbook._reshape_2D(x, 'x')
  520. assert np.shape(xnew) == (1, 5)
  521. x = [[Dummy() for _ in range(5)] for _ in range(3)]
  522. xnew = cbook._reshape_2D(x, 'x')
  523. assert np.shape(xnew) == (3, 5)
  524. # this is strange behaviour, but...
  525. x = np.random.rand(3, 5)
  526. xnew = cbook._reshape_2D(x, 'x')
  527. assert np.shape(xnew) == (5, 3)
  528. # Test a list of lists which are all of length 1
  529. x = [[1], [2], [3]]
  530. xnew = cbook._reshape_2D(x, 'x')
  531. assert isinstance(xnew, list)
  532. assert isinstance(xnew[0], np.ndarray) and xnew[0].shape == (1,)
  533. assert isinstance(xnew[1], np.ndarray) and xnew[1].shape == (1,)
  534. assert isinstance(xnew[2], np.ndarray) and xnew[2].shape == (1,)
  535. # Test a list of zero-dimensional arrays
  536. x = [np.array(0), np.array(1), np.array(2)]
  537. xnew = cbook._reshape_2D(x, 'x')
  538. assert isinstance(xnew, list)
  539. assert len(xnew) == 1
  540. assert isinstance(xnew[0], np.ndarray) and xnew[0].shape == (3,)
  541. # Now test with a list of lists with different lengths, which means the
  542. # array will internally be converted to a 1D object array of lists
  543. x = [[1, 2, 3], [3, 4], [2]]
  544. xnew = cbook._reshape_2D(x, 'x')
  545. assert isinstance(xnew, list)
  546. assert isinstance(xnew[0], np.ndarray) and xnew[0].shape == (3,)
  547. assert isinstance(xnew[1], np.ndarray) and xnew[1].shape == (2,)
  548. assert isinstance(xnew[2], np.ndarray) and xnew[2].shape == (1,)
  549. # We now need to make sure that this works correctly for Numpy subclasses
  550. # where iterating over items can return subclasses too, which may be
  551. # iterable even if they are scalars. To emulate this, we make a Numpy
  552. # array subclass that returns Numpy 'scalars' when iterating or accessing
  553. # values, and these are technically iterable if checking for example
  554. # isinstance(x, collections.abc.Iterable).
  555. class ArraySubclass(np.ndarray):
  556. def __iter__(self):
  557. for value in super().__iter__():
  558. yield np.array(value)
  559. def __getitem__(self, item):
  560. return np.array(super().__getitem__(item))
  561. v = np.arange(10, dtype=float)
  562. x = ArraySubclass((10,), dtype=float, buffer=v.data)
  563. xnew = cbook._reshape_2D(x, 'x')
  564. # We check here that the array wasn't split up into many individual
  565. # ArraySubclass, which is what used to happen due to a bug in _reshape_2D
  566. assert len(xnew) == 1
  567. assert isinstance(xnew[0], ArraySubclass)
  568. # check list of strings:
  569. x = ['a', 'b', 'c', 'c', 'dd', 'e', 'f', 'ff', 'f']
  570. xnew = cbook._reshape_2D(x, 'x')
  571. assert len(xnew[0]) == len(x)
  572. assert isinstance(xnew[0], np.ndarray)
  573. def test_reshape2d_pandas(pd):
  574. # separate to allow the rest of the tests to run if no pandas...
  575. X = np.arange(30).reshape(10, 3)
  576. x = pd.DataFrame(X, columns=["a", "b", "c"])
  577. Xnew = cbook._reshape_2D(x, 'x')
  578. # Need to check each row because _reshape_2D returns a list of arrays:
  579. for x, xnew in zip(X.T, Xnew):
  580. np.testing.assert_array_equal(x, xnew)
  581. def test_reshape2d_xarray(xr):
  582. # separate to allow the rest of the tests to run if no xarray...
  583. X = np.arange(30).reshape(10, 3)
  584. x = xr.DataArray(X, dims=["x", "y"])
  585. Xnew = cbook._reshape_2D(x, 'x')
  586. # Need to check each row because _reshape_2D returns a list of arrays:
  587. for x, xnew in zip(X.T, Xnew):
  588. np.testing.assert_array_equal(x, xnew)
  589. def test_index_of_pandas(pd):
  590. # separate to allow the rest of the tests to run if no pandas...
  591. X = np.arange(30).reshape(10, 3)
  592. x = pd.DataFrame(X, columns=["a", "b", "c"])
  593. Idx, Xnew = cbook.index_of(x)
  594. np.testing.assert_array_equal(X, Xnew)
  595. IdxRef = np.arange(10)
  596. np.testing.assert_array_equal(Idx, IdxRef)
  597. def test_index_of_xarray(xr):
  598. # separate to allow the rest of the tests to run if no xarray...
  599. X = np.arange(30).reshape(10, 3)
  600. x = xr.DataArray(X, dims=["x", "y"])
  601. Idx, Xnew = cbook.index_of(x)
  602. np.testing.assert_array_equal(X, Xnew)
  603. IdxRef = np.arange(10)
  604. np.testing.assert_array_equal(Idx, IdxRef)
  605. def test_contiguous_regions():
  606. a, b, c = 3, 4, 5
  607. # Starts and ends with True
  608. mask = [True]*a + [False]*b + [True]*c
  609. expected = [(0, a), (a+b, a+b+c)]
  610. assert cbook.contiguous_regions(mask) == expected
  611. d, e = 6, 7
  612. # Starts with True ends with False
  613. mask = mask + [False]*e
  614. assert cbook.contiguous_regions(mask) == expected
  615. # Starts with False ends with True
  616. mask = [False]*d + mask[:-e]
  617. expected = [(d, d+a), (d+a+b, d+a+b+c)]
  618. assert cbook.contiguous_regions(mask) == expected
  619. # Starts and ends with False
  620. mask = mask + [False]*e
  621. assert cbook.contiguous_regions(mask) == expected
  622. # No True in mask
  623. assert cbook.contiguous_regions([False]*5) == []
  624. # Empty mask
  625. assert cbook.contiguous_regions([]) == []
  626. def test_safe_first_element_pandas_series(pd):
  627. # deliberately create a pandas series with index not starting from 0
  628. s = pd.Series(range(5), index=range(10, 15))
  629. actual = cbook._safe_first_finite(s)
  630. assert actual == 0
  631. def test_array_patch_perimeters():
  632. # This compares the old implementation as a reference for the
  633. # vectorized one.
  634. def check(x, rstride, cstride):
  635. rows, cols = x.shape
  636. row_inds = [*range(0, rows-1, rstride), rows-1]
  637. col_inds = [*range(0, cols-1, cstride), cols-1]
  638. polys = []
  639. for rs, rs_next in itertools.pairwise(row_inds):
  640. for cs, cs_next in itertools.pairwise(col_inds):
  641. # +1 ensures we share edges between polygons
  642. ps = cbook._array_perimeter(x[rs:rs_next+1, cs:cs_next+1]).T
  643. polys.append(ps)
  644. polys = np.asarray(polys)
  645. assert np.array_equal(polys,
  646. cbook._array_patch_perimeters(
  647. x, rstride=rstride, cstride=cstride))
  648. def divisors(n):
  649. return [i for i in range(1, n + 1) if n % i == 0]
  650. for rows, cols in [(5, 5), (7, 14), (13, 9)]:
  651. x = np.arange(rows * cols).reshape(rows, cols)
  652. for rstride, cstride in itertools.product(divisors(rows - 1),
  653. divisors(cols - 1)):
  654. check(x, rstride=rstride, cstride=cstride)
  655. def test_setattr_cm():
  656. class A:
  657. cls_level = object()
  658. override = object()
  659. def __init__(self):
  660. self.aardvark = 'aardvark'
  661. self.override = 'override'
  662. self._p = 'p'
  663. def meth(self):
  664. ...
  665. @classmethod
  666. def classy(cls):
  667. ...
  668. @staticmethod
  669. def static():
  670. ...
  671. @property
  672. def prop(self):
  673. return self._p
  674. @prop.setter
  675. def prop(self, val):
  676. self._p = val
  677. class B(A):
  678. ...
  679. other = A()
  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 instance every time
  688. assert obj.aardvark is obj.aardvark
  689. assert a.aardvark == 'aardvark'
  690. # and our property happens to give the same instance every time
  691. assert obj.prop is obj.prop
  692. assert obj.cls_level is A.cls_level
  693. assert obj.override == 'override'
  694. assert not hasattr(obj, 'extra')
  695. assert obj.prop == 'p'
  696. assert obj.monkey == other.meth
  697. assert obj.cls_level is A.cls_level
  698. assert 'cls_level' not in obj.__dict__
  699. assert 'classy' not in obj.__dict__
  700. assert 'static' not in obj.__dict__
  701. a = B()
  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, cls_level='bob',
  709. classy='classy', static='static'):
  710. # because we have set a lambda, it is normal attribute access
  711. # and the same every time
  712. assert a.meth is a.meth
  713. assert a.aardvark is a.aardvark
  714. assert a.aardvark == 'moose'
  715. assert a.override == 'boo'
  716. assert a.extra == 'extra'
  717. assert a.prop == 'squirrel'
  718. assert a.monkey != other.meth
  719. assert a.cls_level == 'bob'
  720. assert a.classy == 'classy'
  721. assert a.static == 'static'
  722. verify_pre_post_state(a)
  723. def test_format_approx():
  724. f = cbook._format_approx
  725. assert f(0, 1) == '0'
  726. assert f(0, 2) == '0'
  727. assert f(0, 3) == '0'
  728. assert f(-0.0123, 1) == '-0'
  729. assert f(1e-7, 5) == '0'
  730. assert f(0.0012345600001, 5) == '0.00123'
  731. assert f(-0.0012345600001, 5) == '-0.00123'
  732. assert f(0.0012345600001, 8) == f(0.0012345600001, 10) == '0.00123456'
  733. def test_safe_first_element_with_none():
  734. datetime_lst = [date.today() + timedelta(days=i) for i in range(10)]
  735. datetime_lst[0] = None
  736. actual = cbook._safe_first_finite(datetime_lst)
  737. assert actual is not None and actual == datetime_lst[1]
  738. def test_strip_math():
  739. assert strip_math(r'1 \times 2') == r'1 \times 2'
  740. assert strip_math(r'$1 \times 2$') == '1 x 2'
  741. assert strip_math(r'$\rm{hi}$') == 'hi'
  742. @pytest.mark.parametrize('fmt, value, result', [
  743. ('%.2f m', 0.2, '0.20 m'),
  744. ('{:.2f} m', 0.2, '0.20 m'),
  745. ('{} m', 0.2, '0.2 m'),
  746. ('const', 0.2, 'const'),
  747. ('%d or {}', 0.2, '0 or {}'),
  748. ('{{{:,.0f}}}', 2e5, '{200,000}'),
  749. ('{:.2%}', 2/3, '66.67%'),
  750. ('$%g', 2.54, '$2.54'),
  751. ])
  752. def test_auto_format_str(fmt, value, result):
  753. """Apply *value* to the format string *fmt*."""
  754. assert cbook._auto_format_str(fmt, value) == result
  755. assert cbook._auto_format_str(fmt, np.float64(value)) == result
  756. def test_unpack_to_numpy_from_torch():
  757. """
  758. Test that torch tensors are converted to NumPy arrays.
  759. We don't want to create a dependency on torch in the test suite, so we mock it.
  760. """
  761. class Tensor:
  762. def __init__(self, data):
  763. self.data = data
  764. def __array__(self):
  765. return self.data
  766. torch = ModuleType('torch')
  767. torch.Tensor = Tensor
  768. sys.modules['torch'] = torch
  769. data = np.arange(10)
  770. torch_tensor = torch.Tensor(data)
  771. result = cbook._unpack_to_numpy(torch_tensor)
  772. assert isinstance(result, np.ndarray)
  773. # compare results, do not check for identity: the latter would fail
  774. # if not mocked, and the implementation does not guarantee it
  775. # is the same Python object, just the same values.
  776. assert_array_equal(result, data)
  777. def test_unpack_to_numpy_from_jax():
  778. """
  779. Test that jax arrays are converted to NumPy arrays.
  780. We don't want to create a dependency on jax in the test suite, so we mock it.
  781. """
  782. class Array:
  783. def __init__(self, data):
  784. self.data = data
  785. def __array__(self):
  786. return self.data
  787. jax = ModuleType('jax')
  788. jax.Array = Array
  789. sys.modules['jax'] = jax
  790. data = np.arange(10)
  791. jax_array = jax.Array(data)
  792. result = cbook._unpack_to_numpy(jax_array)
  793. assert isinstance(result, np.ndarray)
  794. # compare results, do not check for identity: the latter would fail
  795. # if not mocked, and the implementation does not guarantee it
  796. # is the same Python object, just the same values.
  797. assert_array_equal(result, data)
  798. def test_unpack_to_numpy_from_tensorflow():
  799. """
  800. Test that tensorflow arrays are converted to NumPy arrays.
  801. We don't want to create a dependency on tensorflow in the test suite, so we mock it.
  802. """
  803. class Tensor:
  804. def __init__(self, data):
  805. self.data = data
  806. def __array__(self):
  807. return self.data
  808. tensorflow = ModuleType('tensorflow')
  809. tensorflow.is_tensor = lambda x: isinstance(x, Tensor)
  810. tensorflow.Tensor = Tensor
  811. sys.modules['tensorflow'] = tensorflow
  812. data = np.arange(10)
  813. tf_tensor = tensorflow.Tensor(data)
  814. result = cbook._unpack_to_numpy(tf_tensor)
  815. assert isinstance(result, np.ndarray)
  816. # compare results, do not check for identity: the latter would fail
  817. # if not mocked, and the implementation does not guarantee it
  818. # is the same Python object, just the same values.
  819. assert_array_equal(result, data)