Skip to content

Style RC parameter #4240

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/matplotlib/rcsetup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions lib/matplotlib/style/__init__.py
Original file line number Diff line number Diff line change
@@ -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'])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is the appropriate place for this call. IIUC, the default style isn't used unless the style module is imported.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is true, but I was not able to put it anywhere else... I tried to import the style from rc on matplotlib import, but it is not possible to do it from the mpl.__init__ as the style module uses some functions from the __init__.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After a small test, I confirmed something I already noticed before, which is that a style call from the matplotlibrc will be applied even without calling the matplotlib.style library. I guess that on init, matplotlib initializes also the style library, or giving the style parameter in the rc will call the style library... I'm not sure about which it is

106 changes: 86 additions & 20 deletions lib/matplotlib/style/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"""
import os
import re
import sys
import contextlib
import warnings

Expand All @@ -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 = {
Expand Down Expand Up @@ -91,26 +92,91 @@ def use(style):
"""
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 = [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])
style = [style]
flattened_style = _flatten_style_dict({PARENT_STYLES: style})
_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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why leave this public?

expand_parent=lambda x: x):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and why leave this with a default value like this?

"""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
Expand Down
153 changes: 150 additions & 3 deletions lib/matplotlib/tests/test_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
Expand Down Expand Up @@ -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()