From 2175cb07b6660e61075eaa97e2317fec11fbe1eb Mon Sep 17 00:00:00 2001 From: Marin Gilles Date: Tue, 17 Mar 2015 13:41:25 +0100 Subject: [PATCH 1/2] 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 --- lib/matplotlib/rcsetup.py | 2 + lib/matplotlib/style/__init__.py | 3 + lib/matplotlib/style/core.py | 110 ++++++++++++++++----- lib/matplotlib/tests/test_style.py | 153 ++++++++++++++++++++++++++++- 4 files changed, 243 insertions(+), 25 deletions(-) diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index b19d234ffa5d..566c3f667b49 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -886,6 +886,8 @@ def validate_animation_writer_path(p): # a map from key -> value, converter defaultParams = { + 'style': [[''], validate_stringlist], + 'backend': ['Agg', validate_backend], # agg is certainly # present 'backend_fallback': [True, validate_bool], # agg is certainly present diff --git a/lib/matplotlib/style/__init__.py b/lib/matplotlib/style/__init__.py index cb0592f41e78..f30f4dd64d96 100644 --- a/lib/matplotlib/style/__init__.py +++ b/lib/matplotlib/style/__init__.py @@ -1,3 +1,6 @@ from __future__ import absolute_import from .core import use, context, available, library, reload_library +from matplotlib import rcParams +if rcParams['style']: + use(rcParams['style']) diff --git a/lib/matplotlib/style/core.py b/lib/matplotlib/style/core.py index 098f45d5b426..54fa6df4ac4b 100644 --- a/lib/matplotlib/style/core.py +++ b/lib/matplotlib/style/core.py @@ -17,6 +17,7 @@ """ import os import re +import sys import contextlib import warnings @@ -33,7 +34,7 @@ USER_LIBRARY_PATHS = [os.path.join(mpl._get_configdir(), 'stylelib')] STYLE_EXTENSION = 'mplstyle' STYLE_FILE_PATTERN = re.compile('([\S]+).%s$' % STYLE_EXTENSION) - +PARENT_STYLES = 'style' # A list of rcParams that should not be applied from styles STYLE_BLACKLIST = { @@ -65,7 +66,7 @@ def _apply_style(d, warn=True): mpl.rcParams.update(_remove_blacklisted_style_params(d, warn=warn)) -def use(style): +def use(styles): """Use matplotlib style settings from a style specification. The style name of 'default' is reserved for reverting back to @@ -89,28 +90,93 @@ def use(style): """ - if cbook.is_string_like(style) or hasattr(style, 'keys'): + if cbook.is_string_like(styles) or hasattr(styles, 'keys'): # If name is a single str or dict, make it a single element list. - styles = [style] - else: - styles = style - - for style in styles: - if not cbook.is_string_like(style): - _apply_style(style) - elif style == 'default': - _apply_style(rcParamsDefault, warn=False) - elif style in library: - _apply_style(library[style]) + styles = [styles] + flattened_style = _flatten_style_dict({PARENT_STYLES: styles}) + _apply_style(flattened_style) + + +def _expand_parent(parent_style): + if cbook.is_string_like(parent_style): + if parent_style == "default": + parent_style = rcParamsDefault else: - try: - rc = rc_params_from_file(style, use_default_template=False) - _apply_style(rc) - except IOError: - msg = ("'%s' not found in the style library and input is " - "not a valid URL or path. See `style.available` for " - "list of available styles.") - raise IOError(msg % style) + parent_style = get_style_dict(parent_style) + return parent_style + + +def flatten_inheritance_dict(child_dict, parent_key, + expand_parent=lambda x: x): + """Return a flattened version of dictionary that inherits from a parent. + + Parameters + ---------- + child_dict : dict + Dictionary with a special key that points to a dictionary of defaults, + or a value that can be expanded to a dictionary of defaults. + parent_key : str + The key that points to a list of parents. + expand_parent : callable(parent) -> dict + Function that returns a dictionary from the value corresponding to + `parent_key`. By default, this simply returns the value. + """ + if parent_key not in child_dict: + return child_dict.copy() + + parents = child_dict[parent_key] + if isinstance(parents, dict): + parents = [parents] + if not isinstance(parents, (list, tuple)): + msg = "Parent value must be list or tuple, but given {!r}" + raise ValueError(msg.format(parents)) + + # Expand any parents defined by `child_dict` into dictionaries. + parents = (expand_parent(p) for p in parents) + + # Resolve any grand-parents defined by parents of `child_dict` + parents = [flatten_inheritance_dict(p, parent_key, expand_parent) + for p in parents] + + # Child will override parent values in `dict.update` so put it last. + ordered_dicts = parents + [child_dict] + + # Copy first dictionary and update with subsequent dictionaries. + output_dict = ordered_dicts[0].copy() + for d in ordered_dicts[1:]: + output_dict.update(d) + + # Since the parent data been resolved, remove parent references. + del output_dict[parent_key] + return output_dict + + +def _flatten_style_dict(style_dict): + return flatten_inheritance_dict(style_dict, PARENT_STYLES, + expand_parent=_expand_parent) + + +def get_style_dict(style): + """Returns a dictionnary containing all the parameters from the + style file. + + Parameters + ---------- + style : str + style from the default library, the personal library or any + full path. + """ + if style in library: + return library[style] + else: + try: + return rc_params_from_file(style, + use_default_template=False) + except IOError: + msg = ("'%s' not found in the style library and input is " + "not a valid URL or path. See `style.available` for " + "list of available styles.") + raise IOError(msg % style) @contextlib.contextmanager diff --git a/lib/matplotlib/tests/test_style.py b/lib/matplotlib/tests/test_style.py index 901218cb76cf..cdfa87205558 100644 --- a/lib/matplotlib/tests/test_style.py +++ b/lib/matplotlib/tests/test_style.py @@ -7,12 +7,16 @@ from collections import OrderedDict from contextlib import contextmanager -from nose.tools import assert_raises +from nose import SkipTest +from nose.tools import assert_raises, assert_equal from nose.plugins.attrib import attr import matplotlib as mpl from matplotlib import style -from matplotlib.style.core import USER_LIBRARY_PATHS, STYLE_EXTENSION +from matplotlib.style.core import (USER_LIBRARY_PATHS, + STYLE_EXTENSION, + BASE_LIBRARY_PATH, + flatten_inheritance_dict, get_style_dict) import six @@ -24,7 +28,8 @@ @contextmanager def temp_style(style_name, settings=None): """Context manager to create a style sheet in a temporary directory.""" - settings = DUMMY_SETTINGS + if not settings: + settings = DUMMY_SETTINGS temp_file = '%s.%s' % (style_name, STYLE_EXTENSION) # Write style settings to file in the temp directory. @@ -130,6 +135,148 @@ def test_context_with_badparam(): assert mpl.rcParams[PARAM] == other_value +def test_get_style_dict(): + style_dict = get_style_dict('bmh') + assert(isinstance(style_dict, dict)) + + +def test_get_style_dict_from_lib(): + style_dict = get_style_dict('bmh') + assert_equal(style_dict['lines.linewidth'], 2.0) + + +def test_get_style_dict_from_file(): + style_dict = get_style_dict(os.path.join(BASE_LIBRARY_PATH, + 'bmh.mplstyle')) + assert_equal(style_dict['lines.linewidth'], 2.0) + + +def test_parent_stylesheet(): + parent_value = 'blue' + parent = {PARAM: parent_value} + child = {'style': parent} + with style.context(child): + assert_equal(mpl.rcParams[PARAM], parent_value) + + +def test_parent_stylesheet_children_override(): + parent_value = 'blue' + child_value = 'gray' + parent = {PARAM: parent_value} + child = {'style': parent, PARAM: child_value} + with style.context(child): + assert_equal(mpl.rcParams[PARAM], child_value) + + +def test_grandparent_stylesheet(): + grandparent_value = 'blue' + grandparent = {PARAM: grandparent_value} + parent = {'style': grandparent} + child = {'style': parent} + with style.context(child): + assert_equal(mpl.rcParams[PARAM], grandparent_value) + + +def test_parent_stylesheet_from_string(): + parent_param = 'lines.linewidth' + parent_value = 2.0 + parent = {parent_param: parent_value} + child = {'style': ['parent']} + with temp_style('parent', settings=parent): + with style.context(child): + assert_equal(mpl.rcParams[parent_param], parent_value) + + +def test_parent_stylesheet_brothers(): + parent_param = PARAM + parent_value1 = 'blue' + parent_value2 = 'gray' + parent1 = {parent_param: parent_value1} + parent2 = {parent_param: parent_value2} + child = {'style': [parent1, parent2]} + with style.context(child): + assert_equal(mpl.rcParams[parent_param], parent_value2) + + +# Dictionnary flattening function tests +def test_empty_dict(): + child = {} + flattened = flatten_inheritance_dict(child, 'parents') + assert_equal(flattened, child) + + +def test_no_parent(): + child = {'my-key': 'my-value'} + flattened = flatten_inheritance_dict(child, 'parents') + assert_equal(flattened, child) + # Verify that flatten_inheritance_dict always returns a copy. + assert(flattened is not child) + + +def test_non_list_raises(): + child = {'parents': 'parent-value'} + assert_raises(ValueError, flatten_inheritance_dict, child, + 'parents') + + +def test_child_with_no_unique_values(): + parent = {'a': 1} + child = {'parents': [parent]} + flattened = flatten_inheritance_dict(child, 'parents') + assert_equal(flattened, parent) + + +def test_child_overrides_parent_value(): + parent = {'a': 'old-value'} + child = {'parents': [parent], 'a': 'new-value'} + flattened = flatten_inheritance_dict(child, 'parents') + assert_equal(flattened, {'a': 'new-value'}) + + +def test_parents_with_distinct_values(): + child = {'parents': [{'a': 1}, {'b': 2}]} + flattened = flatten_inheritance_dict(child, 'parents') + assert_equal(flattened, {'a': 1, 'b': 2}) + + +def test_later_parent_overrides_former(): + child = {'parents': [{'a': 1}, {'a': 2}]} + flattened = flatten_inheritance_dict(child, 'parents') + assert_equal(flattened, {'a': 2}) + + +def test_grandparent(): + grandparent = {'a': 1} + parent = {'parents': [grandparent]} + child = {'parents': [parent]} + flattened = flatten_inheritance_dict(child, 'parents') + assert_equal(flattened, grandparent) + + +def test_custom_expand_parent(): + parent_map = {'a-pointer': {'a': 1}, 'b-pointer': {'b': 2}} + + def expand_parent(key): + return parent_map[key] + + child = {'parents': ['a-pointer', 'b-pointer']} + flattened = flatten_inheritance_dict(child, 'parents', + expand_parent=expand_parent) + assert_equal(flattened, {'a': 1, 'b': 2}) + + +def test_circular_parents(): + parent_map = {'a-pointer': {'parents': ['b-pointer']}, + 'b-pointer': {'parents': ['a-pointer']}} + + def expand_parent(key): + return parent_map[key] + + child = {'parents': ['a-pointer']} + assert_raises(RuntimeError, flatten_inheritance_dict, child, + 'parents', expand_parent=expand_parent) + + if __name__ == '__main__': from numpy import testing testing.run_module_suite() From 105c80a99f28a90bd02973a3ffc54b535dfb4a95 Mon Sep 17 00:00:00 2001 From: Marin Gilles Date: Tue, 22 Mar 2016 20:01:08 +0100 Subject: [PATCH 2/2] API consistency Fix --- lib/matplotlib/style/core.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/style/core.py b/lib/matplotlib/style/core.py index 54fa6df4ac4b..cf0e67eacfb1 100644 --- a/lib/matplotlib/style/core.py +++ b/lib/matplotlib/style/core.py @@ -66,7 +66,7 @@ def _apply_style(d, warn=True): mpl.rcParams.update(_remove_blacklisted_style_params(d, warn=warn)) -def use(styles): +def use(style): """Use matplotlib style settings from a style specification. The style name of 'default' is reserved for reverting back to @@ -90,10 +90,10 @@ def use(styles): """ - if cbook.is_string_like(styles) or hasattr(styles, 'keys'): + if cbook.is_string_like(style) or hasattr(style, 'keys'): # If name is a single str or dict, make it a single element list. - styles = [styles] - flattened_style = _flatten_style_dict({PARENT_STYLES: styles}) + style = [style] + flattened_style = _flatten_style_dict({PARENT_STYLES: style}) _apply_style(flattened_style)