Skip to content

Commit 35ab43a

Browse files
Marin GillesMrngilles
authored andcommitted
Adding the style rc parameter
Allows to support cascading stylesheets directly from the matplorlibrc fixed an issue with depth > 1 added test getting parent styles + sample test styles fixed location of sample styles PEP8 fixes added import of stylesheets in setupext and test_only fixed stylesheet path in test_style other styles path fix other fix for styles path fixed an issue in style.use priority of styles passed added smaller functions for get_parents_styles function Fixed issues concerning parent styles importing other small functions to insert parent styles in the style list + small tweaks simplified the check of dictionnary like object using isinstance Few changes (some refactoring + docstring change) Using @tonysyu dictonnary flattening function to get children styles Added tests + dictionnary flattening fixes Docstring addition PEP8 fix pep8 fix removed style file imports needed for the now removed tests Fix for Py 2.6: some nose function did not exist
1 parent e12d103 commit 35ab43a

4 files changed

Lines changed: 242 additions & 25 deletions

File tree

‎lib/matplotlib/rcsetup.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -826,6 +826,8 @@ def validate_animation_writer_path(p):
826826

827827
# a map from key -> value, converter
828828
defaultParams = {
829+
'style': [[''], validate_stringlist],
830+
829831
'backend': ['Agg', validate_backend], # agg is certainly
830832
# present
831833
'backend_fallback': [True, validate_bool], # agg is certainly present

‎lib/matplotlib/style/__init__.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
from __future__ import absolute_import
22

33
from .core import use, context, available, library, reload_library
4+
from matplotlib import rcParams
5+
if rcParams['style']:
6+
use(rcParams['style'])

‎lib/matplotlib/style/core.py‎

Lines changed: 88 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"""
1818
import os
1919
import re
20+
import sys
2021
import contextlib
2122
import warnings
2223

@@ -33,7 +34,7 @@
3334
USER_LIBRARY_PATHS = [os.path.join(mpl._get_configdir(), 'stylelib')]
3435
STYLE_EXTENSION = 'mplstyle'
3536
STYLE_FILE_PATTERN = re.compile('([\S]+).%s$' % STYLE_EXTENSION)
36-
37+
PARENT_STYLES = 'style'
3738

3839
# A list of rcParams that should not be applied from styles
3940
STYLE_BLACKLIST = set([
@@ -65,7 +66,7 @@ def _apply_style(d, warn=True):
6566
mpl.rcParams.update(_remove_blacklisted_style_params(d, warn=warn))
6667

6768

68-
def use(style):
69+
def use(styles):
6970
"""Use matplotlib style settings from a style specification.
7071
7172
The style name of 'default' is reserved for reverting back to
@@ -89,28 +90,93 @@ def use(style):
8990
9091
9192
"""
92-
if cbook.is_string_like(style) or hasattr(style, 'keys'):
93+
if cbook.is_string_like(styles) or hasattr(styles, 'keys'):
9394
# If name is a single str or dict, make it a single element list.
94-
styles = [style]
95-
else:
96-
styles = style
97-
98-
for style in styles:
99-
if not cbook.is_string_like(style):
100-
_apply_style(style)
101-
elif style == 'default':
102-
_apply_style(rcParamsDefault, warn=False)
103-
elif style in library:
104-
_apply_style(library[style])
95+
styles = [styles]
96+
flattened_style = _flatten_style_dict({PARENT_STYLES: styles})
97+
_apply_style(flattened_style)
98+
99+
100+
def _expand_parent(parent_style):
101+
if cbook.is_string_like(parent_style):
102+
if parent_style == "default":
103+
parent_style = rcParamsDefault
105104
else:
106-
try:
107-
rc = rc_params_from_file(style, use_default_template=False)
108-
_apply_style(rc)
109-
except IOError:
110-
msg = ("'%s' not found in the style library and input is "
111-
"not a valid URL or path. See `style.available` for "
112-
"list of available styles.")
113-
raise IOError(msg % style)
105+
parent_style = get_style_dict(parent_style)
106+
return parent_style
107+
108+
109+
def flatten_inheritance_dict(child_dict, parent_key,
110+
expand_parent=lambda x: x):
111+
"""Return a flattened version of dictionary that inherits from a parent.
112+
113+
Parameters
114+
----------
115+
child_dict : dict
116+
Dictionary with a special key that points to a dictionary of defaults,
117+
or a value that can be expanded to a dictionary of defaults.
118+
parent_key : str
119+
The key that points to a list of parents.
120+
expand_parent : callable(parent) -> dict
121+
Function that returns a dictionary from the value corresponding to
122+
`parent_key`. By default, this simply returns the value.
123+
"""
124+
if parent_key not in child_dict:
125+
return child_dict.copy()
126+
127+
parents = child_dict[parent_key]
128+
if isinstance(parents, dict):
129+
parents = [parents]
130+
if not isinstance(parents, (list, tuple)):
131+
msg = "Parent value must be list or tuple, but given {!r}"
132+
raise ValueError(msg.format(parents))
133+
134+
# Expand any parents defined by `child_dict` into dictionaries.
135+
parents = (expand_parent(p) for p in parents)
136+
137+
# Resolve any grand-parents defined by parents of `child_dict`
138+
parents = [flatten_inheritance_dict(p, parent_key, expand_parent)
139+
for p in parents]
140+
141+
# Child will override parent values in `dict.update` so put it last.
142+
ordered_dicts = parents + [child_dict]
143+
144+
# Copy first dictionary and update with subsequent dictionaries.
145+
output_dict = ordered_dicts[0].copy()
146+
for d in ordered_dicts[1:]:
147+
output_dict.update(d)
148+
149+
# Since the parent data been resolved, remove parent references.
150+
del output_dict[parent_key]
151+
return output_dict
152+
153+
154+
def _flatten_style_dict(style_dict):
155+
return flatten_inheritance_dict(style_dict, PARENT_STYLES,
156+
expand_parent=_expand_parent)
157+
158+
159+
def get_style_dict(style):
160+
"""Returns a dictionnary containing all the parameters from the
161+
style file.
162+
163+
Parameters
164+
----------
165+
style : str
166+
style from the default library, the personal library or any
167+
full path.
168+
"""
169+
if style in library:
170+
return library[style]
171+
else:
172+
try:
173+
return rc_params_from_file(style,
174+
use_default_template=False)
175+
except IOError:
176+
msg = ("'%s' not found in the style library and input is "
177+
"not a valid URL or path. See `style.available` for "
178+
"list of available styles.")
179+
raise IOError(msg % style)
114180

115181

116182
@contextlib.contextmanager

‎lib/matplotlib/tests/test_style.py‎

Lines changed: 149 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@
88
from contextlib import contextmanager
99

1010
from nose import SkipTest
11-
from nose.tools import assert_raises
11+
from nose.tools import assert_raises, assert_equal
1212
from nose.plugins.attrib import attr
1313

1414
import matplotlib as mpl
1515
from matplotlib import style
16-
from matplotlib.style.core import USER_LIBRARY_PATHS, STYLE_EXTENSION
16+
from matplotlib.style.core import (USER_LIBRARY_PATHS,
17+
STYLE_EXTENSION,
18+
BASE_LIBRARY_PATH,
19+
flatten_inheritance_dict, get_style_dict)
1720

1821
from matplotlib.externals import six
1922

@@ -25,7 +28,8 @@
2528
@contextmanager
2629
def temp_style(style_name, settings=None):
2730
"""Context manager to create a style sheet in a temporary directory."""
28-
settings = DUMMY_SETTINGS
31+
if not settings:
32+
settings = DUMMY_SETTINGS
2933
temp_file = '%s.%s' % (style_name, STYLE_EXTENSION)
3034

3135
# Write style settings to file in the temp directory.
@@ -137,6 +141,148 @@ def test_context_with_badparam():
137141
assert mpl.rcParams[PARAM] == other_value
138142

139143

144+
def test_get_style_dict():
145+
style_dict = get_style_dict('bmh')
146+
assert(isinstance(style_dict, dict))
147+
148+
149+
def test_get_style_dict_from_lib():
150+
style_dict = get_style_dict('bmh')
151+
assert_equal(style_dict['lines.linewidth'], 2.0)
152+
153+
154+
def test_get_style_dict_from_file():
155+
style_dict = get_style_dict(os.path.join(BASE_LIBRARY_PATH,
156+
'bmh.mplstyle'))
157+
assert_equal(style_dict['lines.linewidth'], 2.0)
158+
159+
160+
def test_parent_stylesheet():
161+
parent_value = 'blue'
162+
parent = {PARAM: parent_value}
163+
child = {'style': parent}
164+
with style.context(child):
165+
assert_equal(mpl.rcParams[PARAM], parent_value)
166+
167+
168+
def test_parent_stylesheet_children_override():
169+
parent_value = 'blue'
170+
child_value = 'gray'
171+
parent = {PARAM: parent_value}
172+
child = {'style': parent, PARAM: child_value}
173+
with style.context(child):
174+
assert_equal(mpl.rcParams[PARAM], child_value)
175+
176+
177+
def test_grandparent_stylesheet():
178+
grandparent_value = 'blue'
179+
grandparent = {PARAM: grandparent_value}
180+
parent = {'style': grandparent}
181+
child = {'style': parent}
182+
with style.context(child):
183+
assert_equal(mpl.rcParams[PARAM], grandparent_value)
184+
185+
186+
def test_parent_stylesheet_from_string():
187+
parent_param = 'lines.linewidth'
188+
parent_value = 2.0
189+
parent = {parent_param: parent_value}
190+
child = {'style': ['parent']}
191+
with temp_style('parent', settings=parent):
192+
with style.context(child):
193+
assert_equal(mpl.rcParams[parent_param], parent_value)
194+
195+
196+
def test_parent_stylesheet_brothers():
197+
parent_param = PARAM
198+
parent_value1 = 'blue'
199+
parent_value2 = 'gray'
200+
parent1 = {parent_param: parent_value1}
201+
parent2 = {parent_param: parent_value2}
202+
child = {'style': [parent1, parent2]}
203+
with style.context(child):
204+
assert_equal(mpl.rcParams[parent_param], parent_value2)
205+
206+
207+
# Dictionnary flattening function tests
208+
def test_empty_dict():
209+
child = {}
210+
flattened = flatten_inheritance_dict(child, 'parents')
211+
assert_equal(flattened, child)
212+
213+
214+
def test_no_parent():
215+
child = {'my-key': 'my-value'}
216+
flattened = flatten_inheritance_dict(child, 'parents')
217+
assert_equal(flattened, child)
218+
# Verify that flatten_inheritance_dict always returns a copy.
219+
assert(flattened is not child)
220+
221+
222+
def test_non_list_raises():
223+
child = {'parents': 'parent-value'}
224+
assert_raises(ValueError, flatten_inheritance_dict, child,
225+
'parents')
226+
227+
228+
def test_child_with_no_unique_values():
229+
parent = {'a': 1}
230+
child = {'parents': [parent]}
231+
flattened = flatten_inheritance_dict(child, 'parents')
232+
assert_equal(flattened, parent)
233+
234+
235+
def test_child_overrides_parent_value():
236+
parent = {'a': 'old-value'}
237+
child = {'parents': [parent], 'a': 'new-value'}
238+
flattened = flatten_inheritance_dict(child, 'parents')
239+
assert_equal(flattened, {'a': 'new-value'})
240+
241+
242+
def test_parents_with_distinct_values():
243+
child = {'parents': [{'a': 1}, {'b': 2}]}
244+
flattened = flatten_inheritance_dict(child, 'parents')
245+
assert_equal(flattened, {'a': 1, 'b': 2})
246+
247+
248+
def test_later_parent_overrides_former():
249+
child = {'parents': [{'a': 1}, {'a': 2}]}
250+
flattened = flatten_inheritance_dict(child, 'parents')
251+
assert_equal(flattened, {'a': 2})
252+
253+
254+
def test_grandparent():
255+
grandparent = {'a': 1}
256+
parent = {'parents': [grandparent]}
257+
child = {'parents': [parent]}
258+
flattened = flatten_inheritance_dict(child, 'parents')
259+
assert_equal(flattened, grandparent)
260+
261+
262+
def test_custom_expand_parent():
263+
parent_map = {'a-pointer': {'a': 1}, 'b-pointer': {'b': 2}}
264+
265+
def expand_parent(key):
266+
return parent_map[key]
267+
268+
child = {'parents': ['a-pointer', 'b-pointer']}
269+
flattened = flatten_inheritance_dict(child, 'parents',
270+
expand_parent=expand_parent)
271+
assert_equal(flattened, {'a': 1, 'b': 2})
272+
273+
274+
def test_circular_parents():
275+
parent_map = {'a-pointer': {'parents': ['b-pointer']},
276+
'b-pointer': {'parents': ['a-pointer']}}
277+
278+
def expand_parent(key):
279+
return parent_map[key]
280+
281+
child = {'parents': ['a-pointer']}
282+
assert_raises(RuntimeError, flatten_inheritance_dict, child,
283+
'parents', expand_parent=expand_parent)
284+
285+
140286
if __name__ == '__main__':
141287
from numpy import testing
142288
testing.run_module_suite()

0 commit comments

Comments
 (0)