test_rcparams.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  1. from collections import OrderedDict
  2. import copy
  3. import os
  4. from pathlib import Path
  5. import subprocess
  6. import sys
  7. from unittest import mock
  8. from cycler import cycler, Cycler
  9. import pytest
  10. import matplotlib as mpl
  11. from matplotlib import cbook
  12. import matplotlib.pyplot as plt
  13. import matplotlib.colors as mcolors
  14. import numpy as np
  15. from matplotlib.rcsetup import (validate_bool_maybe_none,
  16. validate_stringlist,
  17. validate_colorlist,
  18. validate_color,
  19. validate_bool,
  20. validate_fontweight,
  21. validate_nseq_int,
  22. validate_nseq_float,
  23. validate_cycler,
  24. validate_hatch,
  25. validate_hist_bins,
  26. validate_markevery,
  27. _validate_linestyle)
  28. def test_rcparams(tmpdir):
  29. mpl.rc('text', usetex=False)
  30. mpl.rc('lines', linewidth=22)
  31. usetex = mpl.rcParams['text.usetex']
  32. linewidth = mpl.rcParams['lines.linewidth']
  33. rcpath = Path(tmpdir) / 'test_rcparams.rc'
  34. rcpath.write_text('lines.linewidth: 33')
  35. # test context given dictionary
  36. with mpl.rc_context(rc={'text.usetex': not usetex}):
  37. assert mpl.rcParams['text.usetex'] == (not usetex)
  38. assert mpl.rcParams['text.usetex'] == usetex
  39. # test context given filename (mpl.rc sets linewidth to 33)
  40. with mpl.rc_context(fname=rcpath):
  41. assert mpl.rcParams['lines.linewidth'] == 33
  42. assert mpl.rcParams['lines.linewidth'] == linewidth
  43. # test context given filename and dictionary
  44. with mpl.rc_context(fname=rcpath, rc={'lines.linewidth': 44}):
  45. assert mpl.rcParams['lines.linewidth'] == 44
  46. assert mpl.rcParams['lines.linewidth'] == linewidth
  47. # test rc_file
  48. mpl.rc_file(rcpath)
  49. assert mpl.rcParams['lines.linewidth'] == 33
  50. def test_RcParams_class():
  51. rc = mpl.RcParams({'font.cursive': ['Apple Chancery',
  52. 'Textile',
  53. 'Zapf Chancery',
  54. 'cursive'],
  55. 'font.family': 'sans-serif',
  56. 'font.weight': 'normal',
  57. 'font.size': 12})
  58. expected_repr = """
  59. RcParams({'font.cursive': ['Apple Chancery',
  60. 'Textile',
  61. 'Zapf Chancery',
  62. 'cursive'],
  63. 'font.family': ['sans-serif'],
  64. 'font.size': 12.0,
  65. 'font.weight': 'normal'})""".lstrip()
  66. assert expected_repr == repr(rc)
  67. expected_str = """
  68. font.cursive: ['Apple Chancery', 'Textile', 'Zapf Chancery', 'cursive']
  69. font.family: ['sans-serif']
  70. font.size: 12.0
  71. font.weight: normal""".lstrip()
  72. assert expected_str == str(rc)
  73. # test the find_all functionality
  74. assert ['font.cursive', 'font.size'] == sorted(rc.find_all('i[vz]'))
  75. assert ['font.family'] == list(rc.find_all('family'))
  76. def test_rcparams_update():
  77. rc = mpl.RcParams({'figure.figsize': (3.5, 42)})
  78. bad_dict = {'figure.figsize': (3.5, 42, 1)}
  79. # make sure validation happens on input
  80. with pytest.raises(ValueError), \
  81. pytest.warns(UserWarning, match="validate"):
  82. rc.update(bad_dict)
  83. def test_rcparams_init():
  84. with pytest.raises(ValueError), \
  85. pytest.warns(UserWarning, match="validate"):
  86. mpl.RcParams({'figure.figsize': (3.5, 42, 1)})
  87. def test_Bug_2543():
  88. # Test that it possible to add all values to itself / deepcopy
  89. # This was not possible because validate_bool_maybe_none did not
  90. # accept None as an argument.
  91. # https://github.com/matplotlib/matplotlib/issues/2543
  92. # We filter warnings at this stage since a number of them are raised
  93. # for deprecated rcparams as they should. We don't want these in the
  94. # printed in the test suite.
  95. with cbook._suppress_matplotlib_deprecation_warning():
  96. with mpl.rc_context():
  97. _copy = mpl.rcParams.copy()
  98. for key in _copy:
  99. mpl.rcParams[key] = _copy[key]
  100. with mpl.rc_context():
  101. copy.deepcopy(mpl.rcParams)
  102. # real test is that this does not raise
  103. assert validate_bool_maybe_none(None) is None
  104. assert validate_bool_maybe_none("none") is None
  105. with pytest.raises(ValueError):
  106. validate_bool_maybe_none("blah")
  107. with pytest.raises(ValueError):
  108. validate_bool(None)
  109. with pytest.raises(ValueError):
  110. with mpl.rc_context():
  111. mpl.rcParams['svg.fonttype'] = True
  112. legend_color_tests = [
  113. ('face', {'color': 'r'}, mcolors.to_rgba('r')),
  114. ('face', {'color': 'inherit', 'axes.facecolor': 'r'},
  115. mcolors.to_rgba('r')),
  116. ('face', {'color': 'g', 'axes.facecolor': 'r'}, mcolors.to_rgba('g')),
  117. ('edge', {'color': 'r'}, mcolors.to_rgba('r')),
  118. ('edge', {'color': 'inherit', 'axes.edgecolor': 'r'},
  119. mcolors.to_rgba('r')),
  120. ('edge', {'color': 'g', 'axes.facecolor': 'r'}, mcolors.to_rgba('g'))
  121. ]
  122. legend_color_test_ids = [
  123. 'same facecolor',
  124. 'inherited facecolor',
  125. 'different facecolor',
  126. 'same edgecolor',
  127. 'inherited edgecolor',
  128. 'different facecolor',
  129. ]
  130. @pytest.mark.parametrize('color_type, param_dict, target', legend_color_tests,
  131. ids=legend_color_test_ids)
  132. def test_legend_colors(color_type, param_dict, target):
  133. param_dict[f'legend.{color_type}color'] = param_dict.pop('color')
  134. get_func = f'get_{color_type}color'
  135. with mpl.rc_context(param_dict):
  136. _, ax = plt.subplots()
  137. ax.plot(range(3), label='test')
  138. leg = ax.legend()
  139. assert getattr(leg.legendPatch, get_func)() == target
  140. def test_mfc_rcparams():
  141. mpl.rcParams['lines.markerfacecolor'] = 'r'
  142. ln = mpl.lines.Line2D([1, 2], [1, 2])
  143. assert ln.get_markerfacecolor() == 'r'
  144. def test_mec_rcparams():
  145. mpl.rcParams['lines.markeredgecolor'] = 'r'
  146. ln = mpl.lines.Line2D([1, 2], [1, 2])
  147. assert ln.get_markeredgecolor() == 'r'
  148. def test_axes_titlecolor_rcparams():
  149. mpl.rcParams['axes.titlecolor'] = 'r'
  150. _, ax = plt.subplots()
  151. title = ax.set_title("Title")
  152. assert title.get_color() == 'r'
  153. def test_Issue_1713(tmpdir):
  154. rcpath = Path(tmpdir) / 'test_rcparams.rc'
  155. rcpath.write_text('timezone: UTC', encoding='UTF-32-BE')
  156. with mock.patch('locale.getpreferredencoding', return_value='UTF-32-BE'):
  157. rc = mpl.rc_params_from_file(rcpath, True, False)
  158. assert rc.get('timezone') == 'UTC'
  159. def generate_validator_testcases(valid):
  160. validation_tests = (
  161. {'validator': validate_bool,
  162. 'success': (*((_, True) for _ in
  163. ('t', 'y', 'yes', 'on', 'true', '1', 1, True)),
  164. *((_, False) for _ in
  165. ('f', 'n', 'no', 'off', 'false', '0', 0, False))),
  166. 'fail': ((_, ValueError)
  167. for _ in ('aardvark', 2, -1, [], ))
  168. },
  169. {'validator': validate_stringlist,
  170. 'success': (('', []),
  171. ('a,b', ['a', 'b']),
  172. ('aardvark', ['aardvark']),
  173. ('aardvark, ', ['aardvark']),
  174. ('aardvark, ,', ['aardvark']),
  175. (['a', 'b'], ['a', 'b']),
  176. (('a', 'b'), ['a', 'b']),
  177. (iter(['a', 'b']), ['a', 'b']),
  178. (np.array(['a', 'b']), ['a', 'b']),
  179. ((1, 2), ['1', '2']),
  180. (np.array([1, 2]), ['1', '2']),
  181. ),
  182. 'fail': ((dict(), ValueError),
  183. (1, ValueError),
  184. )
  185. },
  186. {'validator': validate_nseq_int(2),
  187. 'success': ((_, [1, 2])
  188. for _ in ('1, 2', [1.5, 2.5], [1, 2],
  189. (1, 2), np.array((1, 2)))),
  190. 'fail': ((_, ValueError)
  191. for _ in ('aardvark', ('a', 1),
  192. (1, 2, 3)
  193. ))
  194. },
  195. {'validator': validate_nseq_float(2),
  196. 'success': ((_, [1.5, 2.5])
  197. for _ in ('1.5, 2.5', [1.5, 2.5], [1.5, 2.5],
  198. (1.5, 2.5), np.array((1.5, 2.5)))),
  199. 'fail': ((_, ValueError)
  200. for _ in ('aardvark', ('a', 1),
  201. (1, 2, 3)
  202. ))
  203. },
  204. {'validator': validate_cycler,
  205. 'success': (('cycler("color", "rgb")',
  206. cycler("color", 'rgb')),
  207. (cycler('linestyle', ['-', '--']),
  208. cycler('linestyle', ['-', '--'])),
  209. ("""(cycler("color", ["r", "g", "b"]) +
  210. cycler("mew", [2, 3, 5]))""",
  211. (cycler("color", 'rgb') +
  212. cycler("markeredgewidth", [2, 3, 5]))),
  213. ("cycler(c='rgb', lw=[1, 2, 3])",
  214. cycler('color', 'rgb') + cycler('linewidth', [1, 2, 3])),
  215. ("cycler('c', 'rgb') * cycler('linestyle', ['-', '--'])",
  216. (cycler('color', 'rgb') *
  217. cycler('linestyle', ['-', '--']))),
  218. (cycler('ls', ['-', '--']),
  219. cycler('linestyle', ['-', '--'])),
  220. (cycler(mew=[2, 5]),
  221. cycler('markeredgewidth', [2, 5])),
  222. ),
  223. # This is *so* incredibly important: validate_cycler() eval's
  224. # an arbitrary string! I think I have it locked down enough,
  225. # and that is what this is testing.
  226. # TODO: Note that these tests are actually insufficient, as it may
  227. # be that they raised errors, but still did an action prior to
  228. # raising the exception. We should devise some additional tests
  229. # for that...
  230. 'fail': ((4, ValueError), # Gotta be a string or Cycler object
  231. ('cycler("bleh, [])', ValueError), # syntax error
  232. ('Cycler("linewidth", [1, 2, 3])',
  233. ValueError), # only 'cycler()' function is allowed
  234. ('1 + 2', ValueError), # doesn't produce a Cycler object
  235. ('os.system("echo Gotcha")', ValueError), # os not available
  236. ('import os', ValueError), # should not be able to import
  237. ('def badjuju(a): return a; badjuju(cycler("color", "rgb"))',
  238. ValueError), # Should not be able to define anything
  239. # even if it does return a cycler
  240. ('cycler("waka", [1, 2, 3])', ValueError), # not a property
  241. ('cycler(c=[1, 2, 3])', ValueError), # invalid values
  242. ("cycler(lw=['a', 'b', 'c'])", ValueError), # invalid values
  243. (cycler('waka', [1, 3, 5]), ValueError), # not a property
  244. (cycler('color', ['C1', 'r', 'g']), ValueError) # no CN
  245. )
  246. },
  247. {'validator': validate_hatch,
  248. 'success': (('--|', '--|'), ('\\oO', '\\oO'),
  249. ('/+*/.x', '/+*/.x'), ('', '')),
  250. 'fail': (('--_', ValueError),
  251. (8, ValueError),
  252. ('X', ValueError)),
  253. },
  254. {'validator': validate_colorlist,
  255. 'success': (('r,g,b', ['r', 'g', 'b']),
  256. (['r', 'g', 'b'], ['r', 'g', 'b']),
  257. ('r, ,', ['r']),
  258. (['', 'g', 'blue'], ['g', 'blue']),
  259. ([np.array([1, 0, 0]), np.array([0, 1, 0])],
  260. np.array([[1, 0, 0], [0, 1, 0]])),
  261. (np.array([[1, 0, 0], [0, 1, 0]]),
  262. np.array([[1, 0, 0], [0, 1, 0]])),
  263. ),
  264. 'fail': (('fish', ValueError),
  265. ),
  266. },
  267. {'validator': validate_color,
  268. 'success': (('None', 'none'),
  269. ('none', 'none'),
  270. ('AABBCC', '#AABBCC'), # RGB hex code
  271. ('AABBCC00', '#AABBCC00'), # RGBA hex code
  272. ('tab:blue', 'tab:blue'), # named color
  273. ('C12', 'C12'), # color from cycle
  274. ('(0, 1, 0)', [0.0, 1.0, 0.0]), # RGB tuple
  275. ((0, 1, 0), (0, 1, 0)), # non-string version
  276. ('(0, 1, 0, 1)', [0.0, 1.0, 0.0, 1.0]), # RGBA tuple
  277. ((0, 1, 0, 1), (0, 1, 0, 1)), # non-string version
  278. ('(0, 1, "0.5")', [0.0, 1.0, 0.5]), # unusual but valid
  279. ),
  280. 'fail': (('tab:veryblue', ValueError), # invalid name
  281. ('(0, 1)', ValueError), # tuple with length < 3
  282. ('(0, 1, 0, 1, 0)', ValueError), # tuple with length > 4
  283. ('(0, 1, none)', ValueError), # cannot cast none to float
  284. ),
  285. },
  286. {'validator': validate_hist_bins,
  287. 'success': (('auto', 'auto'),
  288. ('fd', 'fd'),
  289. ('10', 10),
  290. ('1, 2, 3', [1, 2, 3]),
  291. ([1, 2, 3], [1, 2, 3]),
  292. (np.arange(15), np.arange(15))
  293. ),
  294. 'fail': (('aardvark', ValueError),
  295. )
  296. },
  297. {'validator': validate_markevery,
  298. 'success': ((None, None),
  299. (1, 1),
  300. (0.1, 0.1),
  301. ((1, 1), (1, 1)),
  302. ((0.1, 0.1), (0.1, 0.1)),
  303. ([1, 2, 3], [1, 2, 3]),
  304. (slice(2), slice(None, 2, None)),
  305. (slice(1, 2, 3), slice(1, 2, 3))
  306. ),
  307. 'fail': (((1, 2, 3), TypeError),
  308. ([1, 2, 0.3], TypeError),
  309. (['a', 2, 3], TypeError),
  310. ([1, 2, 'a'], TypeError),
  311. ((0.1, 0.2, 0.3), TypeError),
  312. ((0.1, 2, 3), TypeError),
  313. ((1, 0.2, 0.3), TypeError),
  314. ((1, 0.1), TypeError),
  315. ((0.1, 1), TypeError),
  316. (('abc'), TypeError),
  317. ((1, 'a'), TypeError),
  318. ((0.1, 'b'), TypeError),
  319. (('a', 1), TypeError),
  320. (('a', 0.1), TypeError),
  321. ('abc', TypeError),
  322. ('a', TypeError),
  323. (object(), TypeError)
  324. )
  325. },
  326. {'validator': _validate_linestyle,
  327. 'success': (('-', '-'), ('solid', 'solid'),
  328. ('--', '--'), ('dashed', 'dashed'),
  329. ('-.', '-.'), ('dashdot', 'dashdot'),
  330. (':', ':'), ('dotted', 'dotted'),
  331. ('', ''), (' ', ' '),
  332. ('None', 'none'), ('none', 'none'),
  333. ('DoTtEd', 'dotted'), # case-insensitive
  334. (['1.23', '4.56'], (None, [1.23, 4.56])),
  335. ([1.23, 456], (None, [1.23, 456.0])),
  336. ([1, 2, 3, 4], (None, [1.0, 2.0, 3.0, 4.0])),
  337. ),
  338. 'fail': (('aardvark', ValueError), # not a valid string
  339. (b'dotted', ValueError),
  340. ('dotted'.encode('utf-16'), ValueError),
  341. ((None, [1, 2]), ValueError), # (offset, dashes) != OK
  342. ((0, [1, 2]), ValueError), # idem
  343. ((-1, [1, 2]), ValueError), # idem
  344. ([1, 2, 3], ValueError), # sequence with odd length
  345. (1.23, ValueError), # not a sequence
  346. )
  347. },
  348. )
  349. for validator_dict in validation_tests:
  350. validator = validator_dict['validator']
  351. if valid:
  352. for arg, target in validator_dict['success']:
  353. yield validator, arg, target
  354. else:
  355. for arg, error_type in validator_dict['fail']:
  356. yield validator, arg, error_type
  357. @pytest.mark.parametrize('validator, arg, target',
  358. generate_validator_testcases(True))
  359. def test_validator_valid(validator, arg, target):
  360. res = validator(arg)
  361. if isinstance(target, np.ndarray):
  362. np.testing.assert_equal(res, target)
  363. elif not isinstance(target, Cycler):
  364. assert res == target
  365. else:
  366. # Cyclers can't simply be asserted equal. They don't implement __eq__
  367. assert list(res) == list(target)
  368. @pytest.mark.parametrize('validator, arg, exception_type',
  369. generate_validator_testcases(False))
  370. def test_validator_invalid(validator, arg, exception_type):
  371. with pytest.raises(exception_type):
  372. validator(arg)
  373. @pytest.mark.parametrize('weight, parsed_weight', [
  374. ('bold', 'bold'),
  375. ('BOLD', ValueError), # weight is case-sensitive
  376. (100, 100),
  377. ('100', 100),
  378. (np.array(100), 100),
  379. # fractional fontweights are not defined. This should actually raise a
  380. # ValueError, but historically did not.
  381. (20.6, 20),
  382. ('20.6', ValueError),
  383. ([100], ValueError),
  384. ])
  385. def test_validate_fontweight(weight, parsed_weight):
  386. if parsed_weight is ValueError:
  387. with pytest.raises(ValueError):
  388. validate_fontweight(weight)
  389. else:
  390. assert validate_fontweight(weight) == parsed_weight
  391. def test_keymaps():
  392. key_list = [k for k in mpl.rcParams if 'keymap' in k]
  393. for k in key_list:
  394. assert isinstance(mpl.rcParams[k], list)
  395. def test_rcparams_reset_after_fail():
  396. # There was previously a bug that meant that if rc_context failed and
  397. # raised an exception due to issues in the supplied rc parameters, the
  398. # global rc parameters were left in a modified state.
  399. with mpl.rc_context(rc={'text.usetex': False}):
  400. assert mpl.rcParams['text.usetex'] is False
  401. with pytest.raises(KeyError):
  402. with mpl.rc_context(rc=OrderedDict([('text.usetex', True),
  403. ('test.blah', True)])):
  404. pass
  405. assert mpl.rcParams['text.usetex'] is False
  406. def test_if_rctemplate_is_up_to_date():
  407. # This tests if the matplotlibrc.template file contains all valid rcParams.
  408. deprecated = {*mpl._all_deprecated, *mpl._deprecated_remain_as_none}
  409. with cbook._get_data_path('matplotlibrc').open() as file:
  410. rclines = file.readlines()
  411. missing = {}
  412. for k, v in mpl.defaultParams.items():
  413. if k[0] == "_":
  414. continue
  415. if k in deprecated:
  416. continue
  417. found = False
  418. for line in rclines:
  419. if k in line:
  420. found = True
  421. if not found:
  422. missing.update({k: v})
  423. if missing:
  424. raise ValueError("The following params are missing in the "
  425. "matplotlibrc.template file: {}"
  426. .format(missing.items()))
  427. def test_if_rctemplate_would_be_valid(tmpdir):
  428. # This tests if the matplotlibrc.template file would result in a valid
  429. # rc file if all lines are uncommented.
  430. with cbook._get_data_path('matplotlibrc').open() as file:
  431. rclines = file.readlines()
  432. newlines = []
  433. for line in rclines:
  434. if line[0] == "#":
  435. newline = line[1:]
  436. else:
  437. newline = line
  438. if "$TEMPLATE_BACKEND" in newline:
  439. newline = "backend : Agg"
  440. if "datapath" in newline:
  441. newline = ""
  442. newlines.append(newline)
  443. d = tmpdir.mkdir('test1')
  444. fname = str(d.join('testrcvalid.temp'))
  445. with open(fname, "w") as f:
  446. f.writelines(newlines)
  447. with pytest.warns(None) as record:
  448. mpl.rc_params_from_file(fname,
  449. fail_on_error=True,
  450. use_default_template=False)
  451. assert len(record) == 0
  452. @pytest.mark.skipif(sys.platform != "linux", reason="Linux only")
  453. def test_backend_fallback_headless(tmpdir):
  454. env = {**os.environ,
  455. "DISPLAY": "", "MPLBACKEND": "", "MPLCONFIGDIR": str(tmpdir)}
  456. with pytest.raises(subprocess.CalledProcessError):
  457. subprocess.run(
  458. [sys.executable, "-c",
  459. "import matplotlib; matplotlib.use('tkagg')"],
  460. env=env, check=True)
  461. @pytest.mark.skipif(sys.platform == "linux" and not os.environ.get("DISPLAY"),
  462. reason="headless")
  463. def test_backend_fallback_headful(tmpdir):
  464. pytest.importorskip("tkinter")
  465. env = {**os.environ, "MPLBACKEND": "", "MPLCONFIGDIR": str(tmpdir)}
  466. backend = subprocess.check_output(
  467. [sys.executable, "-c",
  468. "import matplotlib.pyplot; print(matplotlib.get_backend())"],
  469. env=env, universal_newlines=True)
  470. # The actual backend will depend on what's installed, but at least tkagg is
  471. # present.
  472. assert backend.strip().lower() != "agg"