1from collections import OrderedDict
2from contextlib import contextmanager
3import gc
4from pathlib import Path
5from tempfile import TemporaryDirectory
6import sys
7
8import pytest
9
10import matplotlib as mpl
11from matplotlib import pyplot as plt, style
12from matplotlib.style.core import USER_LIBRARY_PATHS, STYLE_EXTENSION
13
14
15PARAM = 'image.cmap'
16VALUE = 'pink'
17DUMMY_SETTINGS = {PARAM: VALUE}
18
19
20@contextmanager
21def temp_style(style_name, settings=None):
22    """Context manager to create a style sheet in a temporary directory."""
23    if not settings:
24        settings = DUMMY_SETTINGS
25    temp_file = '%s.%s' % (style_name, STYLE_EXTENSION)
26    try:
27        with TemporaryDirectory() as tmpdir:
28            # Write style settings to file in the tmpdir.
29            Path(tmpdir, temp_file).write_text(
30                "\n".join("{}: {}".format(k, v) for k, v in settings.items()))
31            # Add tmpdir to style path and reload so we can access this style.
32            USER_LIBRARY_PATHS.append(tmpdir)
33            style.reload_library()
34            yield
35    finally:
36        style.reload_library()
37
38
39def test_invalid_rc_warning_includes_filename(caplog):
40    SETTINGS = {'foo': 'bar'}
41    basename = 'basename'
42    with temp_style(basename, SETTINGS):
43        # style.reload_library() in temp_style() triggers the warning
44        pass
45    assert (len(caplog.records) == 1
46            and basename in caplog.records[0].getMessage())
47
48
49def test_available():
50    with temp_style('_test_', DUMMY_SETTINGS):
51        assert '_test_' in style.available
52
53
54def test_use():
55    mpl.rcParams[PARAM] = 'gray'
56    with temp_style('test', DUMMY_SETTINGS):
57        with style.context('test'):
58            assert mpl.rcParams[PARAM] == VALUE
59
60
61def test_use_url(tmpdir):
62    path = Path(tmpdir, 'file')
63    path.write_text('axes.facecolor: adeade')
64    with temp_style('test', DUMMY_SETTINGS):
65        url = ('file:'
66               + ('///' if sys.platform == 'win32' else '')
67               + path.resolve().as_posix())
68        with style.context(url):
69            assert mpl.rcParams['axes.facecolor'] == "#adeade"
70
71
72def test_single_path(tmpdir):
73    mpl.rcParams[PARAM] = 'gray'
74    temp_file = f'text.{STYLE_EXTENSION}'
75    path = Path(tmpdir, temp_file)
76    path.write_text(f'{PARAM} : {VALUE}')
77    with style.context(path):
78        assert mpl.rcParams[PARAM] == VALUE
79    assert mpl.rcParams[PARAM] == 'gray'
80
81
82def test_context():
83    mpl.rcParams[PARAM] = 'gray'
84    with temp_style('test', DUMMY_SETTINGS):
85        with style.context('test'):
86            assert mpl.rcParams[PARAM] == VALUE
87    # Check that this value is reset after the exiting the context.
88    assert mpl.rcParams[PARAM] == 'gray'
89
90
91def test_context_with_dict():
92    original_value = 'gray'
93    other_value = 'blue'
94    mpl.rcParams[PARAM] = original_value
95    with style.context({PARAM: other_value}):
96        assert mpl.rcParams[PARAM] == other_value
97    assert mpl.rcParams[PARAM] == original_value
98
99
100def test_context_with_dict_after_namedstyle():
101    # Test dict after style name where dict modifies the same parameter.
102    original_value = 'gray'
103    other_value = 'blue'
104    mpl.rcParams[PARAM] = original_value
105    with temp_style('test', DUMMY_SETTINGS):
106        with style.context(['test', {PARAM: other_value}]):
107            assert mpl.rcParams[PARAM] == other_value
108    assert mpl.rcParams[PARAM] == original_value
109
110
111def test_context_with_dict_before_namedstyle():
112    # Test dict before style name where dict modifies the same parameter.
113    original_value = 'gray'
114    other_value = 'blue'
115    mpl.rcParams[PARAM] = original_value
116    with temp_style('test', DUMMY_SETTINGS):
117        with style.context([{PARAM: other_value}, 'test']):
118            assert mpl.rcParams[PARAM] == VALUE
119    assert mpl.rcParams[PARAM] == original_value
120
121
122def test_context_with_union_of_dict_and_namedstyle():
123    # Test dict after style name where dict modifies the a different parameter.
124    original_value = 'gray'
125    other_param = 'text.usetex'
126    other_value = True
127    d = {other_param: other_value}
128    mpl.rcParams[PARAM] = original_value
129    mpl.rcParams[other_param] = (not other_value)
130    with temp_style('test', DUMMY_SETTINGS):
131        with style.context(['test', d]):
132            assert mpl.rcParams[PARAM] == VALUE
133            assert mpl.rcParams[other_param] == other_value
134    assert mpl.rcParams[PARAM] == original_value
135    assert mpl.rcParams[other_param] == (not other_value)
136
137
138def test_context_with_badparam():
139    original_value = 'gray'
140    other_value = 'blue'
141    d = OrderedDict([(PARAM, original_value), ('badparam', None)])
142    with style.context({PARAM: other_value}):
143        assert mpl.rcParams[PARAM] == other_value
144        x = style.context([d])
145        with pytest.raises(KeyError):
146            with x:
147                pass
148        assert mpl.rcParams[PARAM] == other_value
149
150
151@pytest.mark.parametrize('equiv_styles',
152                         [('mpl20', 'default'),
153                          ('mpl15', 'classic')],
154                         ids=['mpl20', 'mpl15'])
155def test_alias(equiv_styles):
156    rc_dicts = []
157    for sty in equiv_styles:
158        with style.context(sty):
159            rc_dicts.append(mpl.rcParams.copy())
160
161    rc_base = rc_dicts[0]
162    for nm, rc in zip(equiv_styles[1:], rc_dicts[1:]):
163        assert rc_base == rc
164
165
166def test_xkcd_no_cm():
167    assert mpl.rcParams["path.sketch"] is None
168    plt.xkcd()
169    assert mpl.rcParams["path.sketch"] == (1, 100, 2)
170    gc.collect()
171    assert mpl.rcParams["path.sketch"] == (1, 100, 2)
172
173
174def test_xkcd_cm():
175    assert mpl.rcParams["path.sketch"] is None
176    with plt.xkcd():
177        assert mpl.rcParams["path.sketch"] == (1, 100, 2)
178    assert mpl.rcParams["path.sketch"] is None
179