import copy import os import subprocess import sys from unittest import mock from cycler import cycler, Cycler import pytest import matplotlib as mpl from matplotlib import _api, _c_internal_utils import matplotlib.pyplot as plt import matplotlib.colors as mcolors import numpy as np from matplotlib.rcsetup import ( validate_bool, validate_color, validate_colorlist, _validate_color_or_linecolor, validate_cycler, validate_float, validate_fontstretch, validate_fontweight, validate_hatch, validate_hist_bins, validate_int, validate_markevery, validate_stringlist, validate_sketch, _validate_linestyle, _listify_validator) from matplotlib.testing import subprocess_run_for_testing def test_rcparams(tmp_path): mpl.rc('text', usetex=False) mpl.rc('lines', linewidth=22) usetex = mpl.rcParams['text.usetex'] linewidth = mpl.rcParams['lines.linewidth'] rcpath = tmp_path / 'test_rcparams.rc' rcpath.write_text('lines.linewidth: 33', encoding='utf-8') # test context given dictionary with mpl.rc_context(rc={'text.usetex': not usetex}): assert mpl.rcParams['text.usetex'] == (not usetex) assert mpl.rcParams['text.usetex'] == usetex # test context given filename (mpl.rc sets linewidth to 33) with mpl.rc_context(fname=rcpath): assert mpl.rcParams['lines.linewidth'] == 33 assert mpl.rcParams['lines.linewidth'] == linewidth # test context given filename and dictionary with mpl.rc_context(fname=rcpath, rc={'lines.linewidth': 44}): assert mpl.rcParams['lines.linewidth'] == 44 assert mpl.rcParams['lines.linewidth'] == linewidth # test context as decorator (and test reusability, by calling func twice) @mpl.rc_context({'lines.linewidth': 44}) def func(): assert mpl.rcParams['lines.linewidth'] == 44 func() func() # test rc_file mpl.rc_file(rcpath) assert mpl.rcParams['lines.linewidth'] == 33 def test_RcParams_class(): rc = mpl.RcParams({'font.cursive': ['Apple Chancery', 'Textile', 'Zapf Chancery', 'cursive'], 'font.family': 'sans-serif', 'font.weight': 'normal', 'font.size': 12}) expected_repr = """ RcParams({'font.cursive': ['Apple Chancery', 'Textile', 'Zapf Chancery', 'cursive'], 'font.family': ['sans-serif'], 'font.size': 12.0, 'font.weight': 'normal'})""".lstrip() assert expected_repr == repr(rc) expected_str = """ font.cursive: ['Apple Chancery', 'Textile', 'Zapf Chancery', 'cursive'] font.family: ['sans-serif'] font.size: 12.0 font.weight: normal""".lstrip() assert expected_str == str(rc) # test the find_all functionality assert ['font.cursive', 'font.size'] == sorted(rc.find_all('i[vz]')) assert ['font.family'] == list(rc.find_all('family')) def test_rcparams_update(): rc = mpl.RcParams({'figure.figsize': (3.5, 42)}) bad_dict = {'figure.figsize': (3.5, 42, 1)} # make sure validation happens on input with pytest.raises(ValueError): rc.update(bad_dict) def test_rcparams_init(): with pytest.raises(ValueError): mpl.RcParams({'figure.figsize': (3.5, 42, 1)}) def test_nargs_cycler(): from matplotlib.rcsetup import cycler as ccl with pytest.raises(TypeError, match='3 were given'): # cycler() takes 0-2 arguments. ccl(ccl(color=list('rgb')), 2, 3) def test_Bug_2543(): # Test that it possible to add all values to itself / deepcopy # https://github.com/matplotlib/matplotlib/issues/2543 # We filter warnings at this stage since a number of them are raised # for deprecated rcparams as they should. We don't want these in the # printed in the test suite. with _api.suppress_matplotlib_deprecation_warning(): with mpl.rc_context(): _copy = mpl.rcParams.copy() for key in _copy: mpl.rcParams[key] = _copy[key] with mpl.rc_context(): copy.deepcopy(mpl.rcParams) with pytest.raises(ValueError): validate_bool(None) with pytest.raises(ValueError): with mpl.rc_context(): mpl.rcParams['svg.fonttype'] = True legend_color_tests = [ ('face', {'color': 'r'}, mcolors.to_rgba('r')), ('face', {'color': 'inherit', 'axes.facecolor': 'r'}, mcolors.to_rgba('r')), ('face', {'color': 'g', 'axes.facecolor': 'r'}, mcolors.to_rgba('g')), ('edge', {'color': 'r'}, mcolors.to_rgba('r')), ('edge', {'color': 'inherit', 'axes.edgecolor': 'r'}, mcolors.to_rgba('r')), ('edge', {'color': 'g', 'axes.facecolor': 'r'}, mcolors.to_rgba('g')) ] legend_color_test_ids = [ 'same facecolor', 'inherited facecolor', 'different facecolor', 'same edgecolor', 'inherited edgecolor', 'different facecolor', ] @pytest.mark.parametrize('color_type, param_dict, target', legend_color_tests, ids=legend_color_test_ids) def test_legend_colors(color_type, param_dict, target): param_dict[f'legend.{color_type}color'] = param_dict.pop('color') get_func = f'get_{color_type}color' with mpl.rc_context(param_dict): _, ax = plt.subplots() ax.plot(range(3), label='test') leg = ax.legend() assert getattr(leg.legendPatch, get_func)() == target def test_mfc_rcparams(): mpl.rcParams['lines.markerfacecolor'] = 'r' ln = mpl.lines.Line2D([1, 2], [1, 2]) assert ln.get_markerfacecolor() == 'r' def test_mec_rcparams(): mpl.rcParams['lines.markeredgecolor'] = 'r' ln = mpl.lines.Line2D([1, 2], [1, 2]) assert ln.get_markeredgecolor() == 'r' def test_axes_titlecolor_rcparams(): mpl.rcParams['axes.titlecolor'] = 'r' _, ax = plt.subplots() title = ax.set_title("Title") assert title.get_color() == 'r' def test_Issue_1713(tmp_path): rcpath = tmp_path / 'test_rcparams.rc' rcpath.write_text('timezone: UTC', encoding='utf-8') with mock.patch('locale.getpreferredencoding', return_value='UTF-32-BE'): rc = mpl.rc_params_from_file(rcpath, True, False) assert rc.get('timezone') == 'UTC' def test_animation_frame_formats(): # Animation frame_format should allow any of the following # if any of these are not allowed, an exception will be raised # test for gh issue #17908 for fmt in ['png', 'jpeg', 'tiff', 'raw', 'rgba', 'ppm', 'sgi', 'bmp', 'pbm', 'svg']: mpl.rcParams['animation.frame_format'] = fmt def generate_validator_testcases(valid): validation_tests = ( {'validator': validate_bool, 'success': (*((_, True) for _ in ('t', 'y', 'yes', 'on', 'true', '1', 1, True)), *((_, False) for _ in ('f', 'n', 'no', 'off', 'false', '0', 0, False))), 'fail': ((_, ValueError) for _ in ('aardvark', 2, -1, [], )) }, {'validator': validate_stringlist, 'success': (('', []), ('a,b', ['a', 'b']), ('aardvark', ['aardvark']), ('aardvark, ', ['aardvark']), ('aardvark, ,', ['aardvark']), (['a', 'b'], ['a', 'b']), (('a', 'b'), ['a', 'b']), (iter(['a', 'b']), ['a', 'b']), (np.array(['a', 'b']), ['a', 'b']), ), 'fail': ((set(), ValueError), (1, ValueError), ) }, {'validator': _listify_validator(validate_int, n=2), 'success': ((_, [1, 2]) for _ in ('1, 2', [1.5, 2.5], [1, 2], (1, 2), np.array((1, 2)))), 'fail': ((_, ValueError) for _ in ('aardvark', ('a', 1), (1, 2, 3) )) }, {'validator': _listify_validator(validate_float, n=2), 'success': ((_, [1.5, 2.5]) for _ in ('1.5, 2.5', [1.5, 2.5], [1.5, 2.5], (1.5, 2.5), np.array((1.5, 2.5)))), 'fail': ((_, ValueError) for _ in ('aardvark', ('a', 1), (1, 2, 3), (None, ), None)) }, {'validator': validate_cycler, 'success': (('cycler("color", "rgb")', cycler("color", 'rgb')), (cycler('linestyle', ['-', '--']), cycler('linestyle', ['-', '--'])), ("""(cycler("color", ["r", "g", "b"]) + cycler("mew", [2, 3, 5]))""", (cycler("color", 'rgb') + cycler("markeredgewidth", [2, 3, 5]))), ("cycler(c='rgb', lw=[1, 2, 3])", cycler('color', 'rgb') + cycler('linewidth', [1, 2, 3])), ("cycler('c', 'rgb') * cycler('linestyle', ['-', '--'])", (cycler('color', 'rgb') * cycler('linestyle', ['-', '--']))), (cycler('ls', ['-', '--']), cycler('linestyle', ['-', '--'])), (cycler(mew=[2, 5]), cycler('markeredgewidth', [2, 5])), ), # This is *so* incredibly important: validate_cycler() eval's # an arbitrary string! I think I have it locked down enough, # and that is what this is testing. # TODO: Note that these tests are actually insufficient, as it may # be that they raised errors, but still did an action prior to # raising the exception. We should devise some additional tests # for that... 'fail': ((4, ValueError), # Gotta be a string or Cycler object ('cycler("bleh, [])', ValueError), # syntax error ('Cycler("linewidth", [1, 2, 3])', ValueError), # only 'cycler()' function is allowed # do not allow dunder in string literals ("cycler('c', [j.__class__(j) for j in ['r', 'b']])", ValueError), ("cycler('c', [j. __class__(j) for j in ['r', 'b']])", ValueError), ("cycler('c', [j.\t__class__(j) for j in ['r', 'b']])", ValueError), ("cycler('c', [j.\u000c__class__(j) for j in ['r', 'b']])", ValueError), ("cycler('c', [j.__class__(j).lower() for j in ['r', 'b']])", ValueError), ('1 + 2', ValueError), # doesn't produce a Cycler object ('os.system("echo Gotcha")', ValueError), # os not available ('import os', ValueError), # should not be able to import ('def badjuju(a): return a; badjuju(cycler("color", "rgb"))', ValueError), # Should not be able to define anything # even if it does return a cycler ('cycler("waka", [1, 2, 3])', ValueError), # not a property ('cycler(c=[1, 2, 3])', ValueError), # invalid values ("cycler(lw=['a', 'b', 'c'])", ValueError), # invalid values (cycler('waka', [1, 3, 5]), ValueError), # not a property (cycler('color', ['C1', 'r', 'g']), ValueError) # no CN ) }, {'validator': validate_hatch, 'success': (('--|', '--|'), ('\\oO', '\\oO'), ('/+*/.x', '/+*/.x'), ('', '')), 'fail': (('--_', ValueError), (8, ValueError), ('X', ValueError)), }, {'validator': validate_colorlist, 'success': (('r,g,b', ['r', 'g', 'b']), (['r', 'g', 'b'], ['r', 'g', 'b']), ('r, ,', ['r']), (['', 'g', 'blue'], ['g', 'blue']), ([np.array([1, 0, 0]), np.array([0, 1, 0])], np.array([[1, 0, 0], [0, 1, 0]])), (np.array([[1, 0, 0], [0, 1, 0]]), np.array([[1, 0, 0], [0, 1, 0]])), ), 'fail': (('fish', ValueError), ), }, {'validator': validate_color, 'success': (('None', 'none'), ('none', 'none'), ('AABBCC', '#AABBCC'), # RGB hex code ('AABBCC00', '#AABBCC00'), # RGBA hex code ('tab:blue', 'tab:blue'), # named color ('C12', 'C12'), # color from cycle ('(0, 1, 0)', (0.0, 1.0, 0.0)), # RGB tuple ((0, 1, 0), (0, 1, 0)), # non-string version ('(0, 1, 0, 1)', (0.0, 1.0, 0.0, 1.0)), # RGBA tuple ((0, 1, 0, 1), (0, 1, 0, 1)), # non-string version ), 'fail': (('tab:veryblue', ValueError), # invalid name ('(0, 1)', ValueError), # tuple with length < 3 ('(0, 1, 0, 1, 0)', ValueError), # tuple with length > 4 ('(0, 1, none)', ValueError), # cannot cast none to float ('(0, 1, "0.5")', ValueError), # last one not a float ), }, {'validator': _validate_color_or_linecolor, 'success': (('linecolor', 'linecolor'), ('markerfacecolor', 'markerfacecolor'), ('mfc', 'markerfacecolor'), ('markeredgecolor', 'markeredgecolor'), ('mec', 'markeredgecolor') ), 'fail': (('line', ValueError), ('marker', ValueError) ) }, {'validator': validate_hist_bins, 'success': (('auto', 'auto'), ('fd', 'fd'), ('10', 10), ('1, 2, 3', [1, 2, 3]), ([1, 2, 3], [1, 2, 3]), (np.arange(15), np.arange(15)) ), 'fail': (('aardvark', ValueError), ) }, {'validator': validate_markevery, 'success': ((None, None), (1, 1), (0.1, 0.1), ((1, 1), (1, 1)), ((0.1, 0.1), (0.1, 0.1)), ([1, 2, 3], [1, 2, 3]), (slice(2), slice(None, 2, None)), (slice(1, 2, 3), slice(1, 2, 3)) ), 'fail': (((1, 2, 3), TypeError), ([1, 2, 0.3], TypeError), (['a', 2, 3], TypeError), ([1, 2, 'a'], TypeError), ((0.1, 0.2, 0.3), TypeError), ((0.1, 2, 3), TypeError), ((1, 0.2, 0.3), TypeError), ((1, 0.1), TypeError), ((0.1, 1), TypeError), (('abc'), TypeError), ((1, 'a'), TypeError), ((0.1, 'b'), TypeError), (('a', 1), TypeError), (('a', 0.1), TypeError), ('abc', TypeError), ('a', TypeError), (object(), TypeError) ) }, {'validator': _validate_linestyle, 'success': (('-', '-'), ('solid', 'solid'), ('--', '--'), ('dashed', 'dashed'), ('-.', '-.'), ('dashdot', 'dashdot'), (':', ':'), ('dotted', 'dotted'), ('', ''), (' ', ' '), ('None', 'none'), ('none', 'none'), ('DoTtEd', 'dotted'), # case-insensitive ('1, 3', (0, (1, 3))), ([1.23, 456], (0, [1.23, 456.0])), ([1, 2, 3, 4], (0, [1.0, 2.0, 3.0, 4.0])), ((0, [1, 2]), (0, [1, 2])), ((-1, [1, 2]), (-1, [1, 2])), ), 'fail': (('aardvark', ValueError), # not a valid string (b'dotted', ValueError), ('dotted'.encode('utf-16'), ValueError), ([1, 2, 3], ValueError), # sequence with odd length (1.23, ValueError), # not a sequence (("a", [1, 2]), ValueError), # wrong explicit offset ((None, [1, 2]), ValueError), # wrong explicit offset ((1, [1, 2, 3]), ValueError), # odd length sequence (([1, 2], 1), ValueError), # inverted offset/onoff ) }, ) for validator_dict in validation_tests: validator = validator_dict['validator'] if valid: for arg, target in validator_dict['success']: yield validator, arg, target else: for arg, error_type in validator_dict['fail']: yield validator, arg, error_type @pytest.mark.parametrize('validator, arg, target', generate_validator_testcases(True)) def test_validator_valid(validator, arg, target): res = validator(arg) if isinstance(target, np.ndarray): np.testing.assert_equal(res, target) elif not isinstance(target, Cycler): assert res == target else: # Cyclers can't simply be asserted equal. They don't implement __eq__ assert list(res) == list(target) @pytest.mark.parametrize('validator, arg, exception_type', generate_validator_testcases(False)) def test_validator_invalid(validator, arg, exception_type): with pytest.raises(exception_type): validator(arg) @pytest.mark.parametrize('weight, parsed_weight', [ ('bold', 'bold'), ('BOLD', ValueError), # weight is case-sensitive (100, 100), ('100', 100), (np.array(100), 100), # fractional fontweights are not defined. This should actually raise a # ValueError, but historically did not. (20.6, 20), ('20.6', ValueError), ([100], ValueError), ]) def test_validate_fontweight(weight, parsed_weight): if parsed_weight is ValueError: with pytest.raises(ValueError): validate_fontweight(weight) else: assert validate_fontweight(weight) == parsed_weight @pytest.mark.parametrize('stretch, parsed_stretch', [ ('expanded', 'expanded'), ('EXPANDED', ValueError), # stretch is case-sensitive (100, 100), ('100', 100), (np.array(100), 100), # fractional fontweights are not defined. This should actually raise a # ValueError, but historically did not. (20.6, 20), ('20.6', ValueError), ([100], ValueError), ]) def test_validate_fontstretch(stretch, parsed_stretch): if parsed_stretch is ValueError: with pytest.raises(ValueError): validate_fontstretch(stretch) else: assert validate_fontstretch(stretch) == parsed_stretch def test_keymaps(): key_list = [k for k in mpl.rcParams if 'keymap' in k] for k in key_list: assert isinstance(mpl.rcParams[k], list) def test_no_backend_reset_rccontext(): assert mpl.rcParams['backend'] != 'module://aardvark' with mpl.rc_context(): mpl.rcParams['backend'] = 'module://aardvark' assert mpl.rcParams['backend'] == 'module://aardvark' def test_rcparams_reset_after_fail(): # There was previously a bug that meant that if rc_context failed and # raised an exception due to issues in the supplied rc parameters, the # global rc parameters were left in a modified state. with mpl.rc_context(rc={'text.usetex': False}): assert mpl.rcParams['text.usetex'] is False with pytest.raises(KeyError): with mpl.rc_context(rc={'text.usetex': True, 'test.blah': True}): pass assert mpl.rcParams['text.usetex'] is False @pytest.mark.skipif(sys.platform != "linux", reason="Linux only") def test_backend_fallback_headless(tmp_path): env = {**os.environ, "DISPLAY": "", "WAYLAND_DISPLAY": "", "MPLBACKEND": "", "MPLCONFIGDIR": str(tmp_path)} with pytest.raises(subprocess.CalledProcessError): subprocess_run_for_testing( [sys.executable, "-c", "import matplotlib;" "matplotlib.use('tkagg');" "import matplotlib.pyplot;" "matplotlib.pyplot.plot(42);" ], env=env, check=True, stderr=subprocess.DEVNULL) @pytest.mark.skipif( sys.platform == "linux" and not _c_internal_utils.display_is_valid(), reason="headless") def test_backend_fallback_headful(tmp_path): pytest.importorskip("tkinter") env = {**os.environ, "MPLBACKEND": "", "MPLCONFIGDIR": str(tmp_path)} backend = subprocess_run_for_testing( [sys.executable, "-c", "import matplotlib as mpl; " "sentinel = mpl.rcsetup._auto_backend_sentinel; " # Check that access on another instance does not resolve the sentinel. "assert mpl.RcParams({'backend': sentinel})['backend'] == sentinel; " "assert mpl.rcParams._get('backend') == sentinel; " "import matplotlib.pyplot; " "print(matplotlib.get_backend())"], env=env, text=True, check=True, capture_output=True).stdout # The actual backend will depend on what's installed, but at least tkagg is # present. assert backend.strip().lower() != "agg" def test_deprecation(monkeypatch): monkeypatch.setitem( mpl._deprecated_map, "patch.linewidth", ("0.0", "axes.linewidth", lambda old: 2 * old, lambda new: new / 2)) with pytest.warns(mpl.MatplotlibDeprecationWarning): assert mpl.rcParams["patch.linewidth"] \ == mpl.rcParams["axes.linewidth"] / 2 with pytest.warns(mpl.MatplotlibDeprecationWarning): mpl.rcParams["patch.linewidth"] = 1 assert mpl.rcParams["axes.linewidth"] == 2 monkeypatch.setitem( mpl._deprecated_ignore_map, "patch.edgecolor", ("0.0", "axes.edgecolor")) with pytest.warns(mpl.MatplotlibDeprecationWarning): assert mpl.rcParams["patch.edgecolor"] \ == mpl.rcParams["axes.edgecolor"] with pytest.warns(mpl.MatplotlibDeprecationWarning): mpl.rcParams["patch.edgecolor"] = "#abcd" assert mpl.rcParams["axes.edgecolor"] != "#abcd" monkeypatch.setitem( mpl._deprecated_ignore_map, "patch.force_edgecolor", ("0.0", None)) with pytest.warns(mpl.MatplotlibDeprecationWarning): assert mpl.rcParams["patch.force_edgecolor"] is None monkeypatch.setitem( mpl._deprecated_remain_as_none, "svg.hashsalt", ("0.0",)) with pytest.warns(mpl.MatplotlibDeprecationWarning): mpl.rcParams["svg.hashsalt"] = "foobar" assert mpl.rcParams["svg.hashsalt"] == "foobar" # Doesn't warn. mpl.rcParams["svg.hashsalt"] = None # Doesn't warn. mpl.rcParams.update(mpl.rcParams.copy()) # Doesn't warn. # Note that the warning suppression actually arises from the # iteration over the updater rcParams being protected by # suppress_matplotlib_deprecation_warning, rather than any explicit check. @pytest.mark.parametrize("value", [ "best", 1, "1", (0.9, .7), (-0.9, .7), "(0.9, .7)" ]) def test_rcparams_legend_loc(value): # rcParams['legend.loc'] should allow any of the following formats. # if any of these are not allowed, an exception will be raised # test for gh issue #22338 mpl.rcParams["legend.loc"] = value @pytest.mark.parametrize("value", [ "best", 1, (0.9, .7), (-0.9, .7), ]) def test_rcparams_legend_loc_from_file(tmp_path, value): # rcParams['legend.loc'] should be settable from matplotlibrc. # if any of these are not allowed, an exception will be raised. # test for gh issue #22338 rc_path = tmp_path / "matplotlibrc" rc_path.write_text(f"legend.loc: {value}") with mpl.rc_context(fname=rc_path): assert mpl.rcParams["legend.loc"] == value @pytest.mark.parametrize("value", [(1, 2, 3), '1, 2, 3', '(1, 2, 3)']) def test_validate_sketch(value): mpl.rcParams["path.sketch"] = value assert mpl.rcParams["path.sketch"] == (1, 2, 3) assert validate_sketch(value) == (1, 2, 3) @pytest.mark.parametrize("value", [1, '1', '1 2 3']) def test_validate_sketch_error(value): with pytest.raises(ValueError, match="scale, length, randomness"): validate_sketch(value) with pytest.raises(ValueError, match="scale, length, randomness"): mpl.rcParams["path.sketch"] = value @pytest.mark.parametrize("value", ['1, 2, 3', '(1,2,3)']) def test_rcparams_path_sketch_from_file(tmp_path, value): rc_path = tmp_path / "matplotlibrc" rc_path.write_text(f"path.sketch: {value}") with mpl.rc_context(fname=rc_path): assert mpl.rcParams["path.sketch"] == (1, 2, 3)