From 8b0f5aa188153343cd7400f33deb8a92e5750d00 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sun, 1 Dec 2013 20:32:29 -0600 Subject: [PATCH 01/10] Set default values of kwargs on the fly Three functions for handling setting, updating, and resetting kwargs on member functions of classes. This is done by monkey-patching wrapped functions into the classes. This will work for both explicit kwargs and pass-through kwargs. --- lib/matplotlib/rcparam_ng.py | 145 +++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 lib/matplotlib/rcparam_ng.py diff --git a/lib/matplotlib/rcparam_ng.py b/lib/matplotlib/rcparam_ng.py new file mode 100644 index 000000000000..ada775adcb17 --- /dev/null +++ b/lib/matplotlib/rcparam_ng.py @@ -0,0 +1,145 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) +import six +from collections import namedtuple +from copy import copy + +from functools import wraps + +_kw_dict_nm = '_kw_defaults' +_kw_entry = namedtuple('_kw_entry', ['orig_funtion', 'kw_dict']) + + +def set_defaults(cls, key, new_defaults): + """ + Set a set of default kwargs for the function `key` on + the class `cls`. + + If there are currently defaults set, they will be removed + before `new_defaults` are set. + + Parameters + ---------- + cls : class + The class that `key` is a member function on + + key : str + name of the function to set the default values for + + new_defaults : dict + kwargs to set as the default + """ + # if the class doesn't have this key, raise an exception + if not hasattr(cls, key): + raise ValueError(("The class {cls} does not have attribute" + + "{key}").format(cls=cls, key=key)) + + # make sure the class has the persistent structure + # saving the original function + if not hasattr(cls, _kw_dict_nm): + setattr(cls, _kw_dict_nm, dict()) + + if not six.callable(getattr(cls, key)): + raise ValueError("The attribute {key} of {cls} ".format(key=key, + cls=cls) + + "is not callable") + + kw_dict = getattr(cls, _kw_dict_nm) + + if key in kw_dict: + orig_fun, old_dict = kw_dict.pop(key) + else: + orig_fun = getattr(cls, key) + + # make a copy of the input so we don't have to worry about side effects + # or external changes + new_defaults = copy(new_defaults) + + # make dictionary entry and shove into the dictionary + kw_dict[key] = _kw_entry(orig_fun, new_defaults) + + # make wrapper function, closes over the copied dictionary + @wraps(orig_fun) + def wrapper(*args, **kwargs): + for k, v in new_defaults.iteritems(): + if k not in kwargs: + kwargs[k] = v + return orig_fun(*args, **kwargs) + + setattr(cls, key, wrapper) + + +def update_defaults(cls, key, new_defaults): + """ + Updates the default values set for the `key` method + of the class `cls`. + + If no default values are currently set, set defaults + to `new_defaults`, if there are currently defaults set + update with the values in `new_defaults` + + Parameters + ---------- + cls : class + The class that `key` is a member function on + + key : str + name of the function to set the default values for + + new_defaults : dict + kwargs to set as the default + """ + # if the class doesn't have this key, raise an exception + if not hasattr(cls, key): + raise ValueError(("The class {cls} does not have attribute" + + "{key}").format(cls=cls, key=key)) + + # if there isn't the persistent structure, then no default is + # set, call `set_defaults` and return + if not hasattr(cls, _kw_dict_nm): + set_defaults(cls, key, new_defaults) + return + # grab the persistent dict + kw_dict = getattr(cls, _kw_dict_nm) + # if key in the persistent structure + if key in kw_dict: + # grab the existing dict + orig_fun, old_dict = kw_dict[key] + # update it + old_dict.update(new_defaults) + else: + # otherwise, pass on to `set_defaults` and return + set_defaults(cls, key, new_defaults) + return + + +def reset_defaults(cls, key): + """ + Removes any set defaults from the function `key` on + the class `cls`. + + Parameters + ---------- + cls : class + The class that `key` is a member function on + + key : str + name of the function to set the default values for + """ + # if the class doesn't have this key, raise an exception + if not hasattr(cls, key): + raise ValueError(("The class {cls} does not have attribute" + + "{key}").format(cls=cls, key=key)) + + # if there isn't the persistent structure, then no default to + # reset, return doing nothing + if not hasattr(cls, _kw_dict_nm): + return + # + kw_dict = getattr(cls, _kw_dict_nm) + + if key in kw_dict: + # grab the original function + orig_fun, old_dict = kw_dict.pop(key) + # reset to the original function + setattr(cls, key, orig_fun) From 5963b00b05e4c82bb2e7eec8c020792bd69edcab Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sun, 1 Dec 2013 22:38:00 -0600 Subject: [PATCH 02/10] Added a class to manage collecting/applying the kwarg dictionaries. Includes dumping to/loading from json files. --- lib/matplotlib/rcparam_ng.py | 117 ++++++++++++++++++++++++++++++++++- 1 file changed, 116 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/rcparam_ng.py b/lib/matplotlib/rcparam_ng.py index ada775adcb17..d16771cc8fcb 100644 --- a/lib/matplotlib/rcparam_ng.py +++ b/lib/matplotlib/rcparam_ng.py @@ -1,15 +1,19 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) import six -from collections import namedtuple +from collections import namedtuple, defaultdict from copy import copy +import json from functools import wraps +import matplotlib + _kw_dict_nm = '_kw_defaults' _kw_entry = namedtuple('_kw_entry', ['orig_funtion', 'kw_dict']) + def set_defaults(cls, key, new_defaults): """ Set a set of default kwargs for the function `key` on @@ -143,3 +147,114 @@ def reset_defaults(cls, key): orig_fun, old_dict = kw_dict.pop(key) # reset to the original function setattr(cls, key, orig_fun) + +def string_to_class(klass): + """ + Turns a string -> a class object + """ + last_level = matplotlib + # split the string + split_klass = klass.split('.') + # strip the matplotlib off the front + if split_klass[0] == 'matplotlib': + split_klass.pop(0) + + for _k in split_klass: + if not hasattr(last_level, _k): + raise ValueError("not valid, make msg better") + last_level = getattr(last_level, _k) + if not isinstance(last_level, type): + raise ValueError("not valid, make msg better") + + return last_level + +class RcParamsNG(object): + """ + A class for keeping track of default values + """ + def __init__(self, input_dict=None): + """ + Parameters + ---------- + input_dict : dict + a dict of dicts. Top level keys are strings from hte classes + inner keys are function names, inner values are kwarg diccts + + """ + self.core_dict = defaultdict(dict) + if input_dict is not None: + self.core_dict.update(input_dict) + + def store_default(self, klass, key, new_defaults): + """ + Adds an entry to the core for the given values + + Parameters + ---------- + klass : str + string name of class to set defaults for + + key : str + function to set the defaults for + + new_defaults : dict + dict containing the new defaults (kwarg pairs) + """ + self.core_dict[klass][key] = new_defaults + + def set_defaults(self): + """ + Set the defaults contained in this object. Use `set_defaults` + which removes any existing defaults, leaving only the values + in this object in place. + """ + # loop over the core dictionary + for klass, kw_pair in six.iteritems(self.core_dict): + # turn the string into a class + cls = string_to_class(klass) + # look over the list of keys and set the defaults + for key, default_dict in six.iteritems(kw_pair): + set_defaults(cls, key, default_dict) + + def update_defaults(self): + """ + Update to the default values contained in this object. + Use `update_defaults` which leaves non-conflicting defaults + in place. + """ + # loop over the core dictionary + for klass, kw_pair in six.iteritems(self.core_dict): + # turn the string into a class + cls = string_to_class(klass) + # look over the list of keys and set the defaults + for key, default_dict in six.iteritems(kw_pair): + update_defaults(cls, key, default_dict) + + def to_json(self, out_file_path): + """ + Dumps default values to json file. Use `from_json` to + recover. + + Parameters + ---------- + out_file_path : str + A valid path to save the json file to. Will overwrite + any existing file at path + """ + with open(out_file_path, 'w') as fout: + json.dump(self.core_dict, fout, ensure_ascii=False) + + @classmethod + def from_json(cls, in_file_path): + """ + Creates a new RcParamsNG object from a json file. (see + `to_json` to dump to json). + + Parameters + ---------- + in_file_path : str + Path to a json file to load. + """ + with open(in_file_path, 'r') as fin: + in_dict = json.load(fin) + return cls(in_dict) From c2f61254912a9152bb8aa2ed0f1438a19ededcd4 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sun, 1 Dec 2013 23:07:54 -0600 Subject: [PATCH 03/10] made json dumped files human readable --- lib/matplotlib/rcparam_ng.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/rcparam_ng.py b/lib/matplotlib/rcparam_ng.py index d16771cc8fcb..fd6079646d82 100644 --- a/lib/matplotlib/rcparam_ng.py +++ b/lib/matplotlib/rcparam_ng.py @@ -242,7 +242,8 @@ def to_json(self, out_file_path): any existing file at path """ with open(out_file_path, 'w') as fout: - json.dump(self.core_dict, fout, ensure_ascii=False) + json.dump(self.core_dict, fout, ensure_ascii=False, + indent=4) @classmethod def from_json(cls, in_file_path): From d14d49865f8fd05881592f712a5f02ed458d92b9 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sun, 1 Dec 2013 23:34:08 -0600 Subject: [PATCH 04/10] pep8 fixes --- lib/matplotlib/rcparam_ng.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/rcparam_ng.py b/lib/matplotlib/rcparam_ng.py index fd6079646d82..6c4194e8bba1 100644 --- a/lib/matplotlib/rcparam_ng.py +++ b/lib/matplotlib/rcparam_ng.py @@ -13,7 +13,6 @@ _kw_entry = namedtuple('_kw_entry', ['orig_funtion', 'kw_dict']) - def set_defaults(cls, key, new_defaults): """ Set a set of default kwargs for the function `key` on @@ -148,6 +147,7 @@ def reset_defaults(cls, key): # reset to the original function setattr(cls, key, orig_fun) + def string_to_class(klass): """ Turns a string -> a class object @@ -168,6 +168,7 @@ def string_to_class(klass): return last_level + class RcParamsNG(object): """ A class for keeping track of default values From 8bd3a27336242590898f90b3ac88bf4a873ff5dd Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 12 Dec 2013 22:52:55 -0600 Subject: [PATCH 05/10] made wrapped function override kwarg values of `None` --- lib/matplotlib/rcparam_ng.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/rcparam_ng.py b/lib/matplotlib/rcparam_ng.py index 6c4194e8bba1..5e5b3dba8bf8 100644 --- a/lib/matplotlib/rcparam_ng.py +++ b/lib/matplotlib/rcparam_ng.py @@ -62,10 +62,11 @@ def set_defaults(cls, key, new_defaults): kw_dict[key] = _kw_entry(orig_fun, new_defaults) # make wrapper function, closes over the copied dictionary + # and original function @wraps(orig_fun) def wrapper(*args, **kwargs): for k, v in new_defaults.iteritems(): - if k not in kwargs: + if k not in kwargs and kwargs[k] is not None: kwargs[k] = v return orig_fun(*args, **kwargs) From ce6a8cfca8ae81a1d11fafb096108f76baf219b6 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 16 Jan 2014 13:30:12 -0500 Subject: [PATCH 06/10] fixed logic as @tonysyu pointed out --- lib/matplotlib/rcparam_ng.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/rcparam_ng.py b/lib/matplotlib/rcparam_ng.py index 5e5b3dba8bf8..3b1a67aa5cf4 100644 --- a/lib/matplotlib/rcparam_ng.py +++ b/lib/matplotlib/rcparam_ng.py @@ -66,7 +66,7 @@ def set_defaults(cls, key, new_defaults): @wraps(orig_fun) def wrapper(*args, **kwargs): for k, v in new_defaults.iteritems(): - if k not in kwargs and kwargs[k] is not None: + if k not in kwargs or kwargs[k] is None: kwargs[k] = v return orig_fun(*args, **kwargs) From fb1fdf75d9512e888f7a56e62110b0a5cb1ed8fa Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Thu, 16 Jan 2014 22:03:32 -0600 Subject: [PATCH 07/10] Move configuration class --- lib/matplotlib/config/__init__.py | 0 lib/matplotlib/{rcparam_ng.py => config/mpl_config.py} | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 lib/matplotlib/config/__init__.py rename lib/matplotlib/{rcparam_ng.py => config/mpl_config.py} (98%) diff --git a/lib/matplotlib/config/__init__.py b/lib/matplotlib/config/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/lib/matplotlib/rcparam_ng.py b/lib/matplotlib/config/mpl_config.py similarity index 98% rename from lib/matplotlib/rcparam_ng.py rename to lib/matplotlib/config/mpl_config.py index 3b1a67aa5cf4..3920a7f0ea82 100644 --- a/lib/matplotlib/rcparam_ng.py +++ b/lib/matplotlib/config/mpl_config.py @@ -170,7 +170,7 @@ def string_to_class(klass): return last_level -class RcParamsNG(object): +class MPLConfig(object): """ A class for keeping track of default values """ @@ -250,7 +250,7 @@ def to_json(self, out_file_path): @classmethod def from_json(cls, in_file_path): """ - Creates a new RcParamsNG object from a json file. (see + Creates a new MPLConfig object from a json file. (see `to_json` to dump to json). Parameters From 8caa77d61c918e4825c3e7a0c6a396a1f081475a Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Thu, 16 Jan 2014 22:25:17 -0600 Subject: [PATCH 08/10] Add parsing functions to transform user-config to mpl-config --- lib/matplotlib/config/config_alias_map.json | 8 +++ lib/matplotlib/config/parse_user_config.py | 66 +++++++++++++++++++++ lib/matplotlib/tests/test_config.py | 27 +++++++++ 3 files changed, 101 insertions(+) create mode 100644 lib/matplotlib/config/config_alias_map.json create mode 100644 lib/matplotlib/config/parse_user_config.py create mode 100644 lib/matplotlib/tests/test_config.py diff --git a/lib/matplotlib/config/config_alias_map.json b/lib/matplotlib/config/config_alias_map.json new file mode 100644 index 000000000000..f036a3c241b5 --- /dev/null +++ b/lib/matplotlib/config/config_alias_map.json @@ -0,0 +1,8 @@ +{ +"lines.linewidth": + [ + "collections.LineCollection:__init__:linewidths", + "contour.ContourSet:__init__:linewidths", + "lines.Line2D:__init__:linewidth" + ] +} diff --git a/lib/matplotlib/config/parse_user_config.py b/lib/matplotlib/config/parse_user_config.py new file mode 100644 index 000000000000..cb63b0254ff8 --- /dev/null +++ b/lib/matplotlib/config/parse_user_config.py @@ -0,0 +1,66 @@ +import os +import json + +import six + + +LOCAL_DIR = os.path.dirname(os.path.abspath(__file__)) +DEFAULT_ALIAS_MAPPING = os.path.join(LOCAL_DIR, 'config_alias_map.json') +ALIAS_MAPPING = {} + + +def update_config_from_dict_path(config_dict, dict_path, value): + """Set value in a config dictionary using a path string. + + Parameters + ---------- + config_dict : dict + Configuration dictionary matching format expected by + ``matplotlib.config.mpl_config.MPLConfig``. + dict_path : str + String with nested dictionary keys separated by a colon. + For example, 'a:b:c' maps to the key ``some_dict['a']['b']['c']``. + value : object + Configuration value. + """ + dict_keys = dict_path.split(':') + key_to_set = dict_keys.pop() + + inner_dict = config_dict + for key in dict_keys: + if key not in inner_dict: + inner_dict[key] = {} + inner_dict = inner_dict[key] + inner_dict[key_to_set] = value + + +def load_config_mapping(filename): + """Return dictionary mapping config labels to config paths. + """ + with open(filename) as f: + config_mapping = json.load(f) + return config_mapping + + +def update_alias_mapping(filename): + """Update mappings from user-config aliases to config dict paths. """ + ALIAS_MAPPING.update(load_config_mapping(filename)) + +update_alias_mapping(DEFAULT_ALIAS_MAPPING) + + +def user_key_to_dict_paths(key): + """Return config-dict paths from user-config alias. + + See also ``update_config_from_dict_path``. + """ + return ALIAS_MAPPING[key] + + +def update_config_dict_from_user_config(config_dict, user_config): + """Update internal configuration dict from user-config dict. + """ + for user_key, value in six.iteritems(user_config): + dict_paths = user_key_to_dict_paths(user_key) + for path in dict_paths: + update_config_from_dict_path(config_dict, path, value) diff --git a/lib/matplotlib/tests/test_config.py b/lib/matplotlib/tests/test_config.py new file mode 100644 index 000000000000..6dfef22f7c62 --- /dev/null +++ b/lib/matplotlib/tests/test_config.py @@ -0,0 +1,27 @@ +from matplotlib.config.parse_user_config import ( + update_config_from_dict_path, update_config_dict_from_user_config +) + + +def test_set_config_dict_path(): + config_dict = {} + update_config_from_dict_path(config_dict, 'a:b:c', 1) + assert config_dict['a']['b']['c'] == 1 + + +def test_set_config_dict_values_user_config(): + user_config = {'lines.linewidth': 100} + config_dict = {} + update_config_dict_from_user_config(config_dict, user_config) + + value = config_dict['collections.LineCollection']['__init__']['linewidths'] + assert value == 100 + value = config_dict['contour.ContourSet']['__init__']['linewidths'] + assert value == 100 + value = config_dict['lines.Line2D']['__init__']['linewidth'] + assert value == 100 + + +if __name__ == '__main__': + from numpy import testing + testing.run_module_suite() From a3a108304b226f28420f4a97ee6adfe186c21c63 Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Thu, 16 Jan 2014 22:26:48 -0600 Subject: [PATCH 09/10] Connect MPLConfig to user-config parser --- examples/style_sheets/plot_mpl_config.py | 10 ++++++++++ lib/matplotlib/config/mpl_config.py | 20 +++++++++++++++++--- 2 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 examples/style_sheets/plot_mpl_config.py diff --git a/examples/style_sheets/plot_mpl_config.py b/examples/style_sheets/plot_mpl_config.py new file mode 100644 index 000000000000..dcd4b3cf7872 --- /dev/null +++ b/examples/style_sheets/plot_mpl_config.py @@ -0,0 +1,10 @@ +import matplotlib.pyplot as plt +from matplotlib.config.mpl_config import MPLConfig + +user_config = {'lines.linewidth': 10} + +mplrc = MPLConfig.from_user_config(user_config) +mplrc.set_defaults() + +plt.plot([1, 2, 3]) +plt.show() diff --git a/lib/matplotlib/config/mpl_config.py b/lib/matplotlib/config/mpl_config.py index 3920a7f0ea82..c75361050a5c 100644 --- a/lib/matplotlib/config/mpl_config.py +++ b/lib/matplotlib/config/mpl_config.py @@ -8,6 +8,8 @@ from functools import wraps import matplotlib +from .parse_user_config import update_config_dict_from_user_config + _kw_dict_nm = '_kw_defaults' _kw_entry = namedtuple('_kw_entry', ['orig_funtion', 'kw_dict']) @@ -149,6 +151,11 @@ def reset_defaults(cls, key): setattr(cls, key, orig_fun) +def raise_invalid_class_path_error(class_parts): + class_path = '.'.join(class_parts) + raise ValueError("Invalid class: %s" % class_path) + + def string_to_class(klass): """ Turns a string -> a class object @@ -162,10 +169,11 @@ def string_to_class(klass): for _k in split_klass: if not hasattr(last_level, _k): - raise ValueError("not valid, make msg better") + raise_invalid_class_path_error(split_klass) last_level = getattr(last_level, _k) - if not isinstance(last_level, type): - raise ValueError("not valid, make msg better") + + if not isinstance(last_level, object): + raise_invalid_class_path_error(split_klass) return last_level @@ -261,3 +269,9 @@ def from_json(cls, in_file_path): with open(in_file_path, 'r') as fin: in_dict = json.load(fin) return cls(in_dict) + + @classmethod + def from_user_config(cls, user_config): + config_dict = {} + update_config_dict_from_user_config(config_dict, user_config) + return cls(config_dict) From e0eee67a370b5e4d1e962a50779a47395374327c Mon Sep 17 00:00:00 2001 From: Tony S Yu Date: Fri, 17 Jan 2014 07:29:27 -0600 Subject: [PATCH 10/10] Fix class check. --- lib/matplotlib/config/mpl_config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/config/mpl_config.py b/lib/matplotlib/config/mpl_config.py index c75361050a5c..98a8841c7ad9 100644 --- a/lib/matplotlib/config/mpl_config.py +++ b/lib/matplotlib/config/mpl_config.py @@ -3,6 +3,7 @@ import six from collections import namedtuple, defaultdict from copy import copy +import inspect import json from functools import wraps @@ -172,7 +173,7 @@ def string_to_class(klass): raise_invalid_class_path_error(split_klass) last_level = getattr(last_level, _k) - if not isinstance(last_level, object): + if not inspect.isclass(last_level): raise_invalid_class_path_error(split_klass) return last_level