# Copyright (C) 2006-2012 Johan Hake
#
# This file is part of ModelParameters.
#
# ModelParameters is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ModelParameters is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with ModelParameters. If not, see <http://www.gnu.org/licenses/>.
"""
Contains the ParameterDict class, useful for defining
recursive dictionaries of parameters and using attribute
syntax for later access.
"""
__all__ = ["Param", "ScalarParam", "OptionParam", "ConstParam", "ArrayParam", \
"SlaveParam", "inf", "ParameterDict"]
import six
cmp = lambda x, y: (x > y) - (x < y)
# System imports
try:
from .sympytools import sp
except ImportError as e:
sp = None
def rjust(s, *args, **kwargs):
return s.rjust(*args, **kwargs)
def ljust(s, *args, **kwargs):
return s.ljust(*args, **kwargs)
def center(s, *args, **kwargs):
return s.center(*args, **kwargs)
# local imports
from .parameters import *
from .logger import *
from .utils import check_arg, check_kwarg, scalars, value_formatter,\
Range, tuplewrap, integers, nptypes, inf, VALUE_JUST
KEY_JUST = ljust
PAR_PREFIX = "--"
FORMAT_CONVERTER = {int:"int", float:"float", str:"string", \
list:None, tuple:None, bool:"int"}
def cmp_to_key(mycmp):
'Convert a cmp= function into a key= function'
class K(object):
def __init__(self, obj, *args):
self.obj = obj
def __lt__(self, other):
return mycmp(self.obj, other.obj) < 0
def __gt__(self, other):
return mycmp(self.obj, other.obj) > 0
def __eq__(self, other):
return mycmp(self.obj, other.obj) == 0
def __le__(self, other):
return mycmp(self.obj, other.obj) <= 0
def __ge__(self, other):
return mycmp(self.obj, other.obj) >= 0
def __ne__(self, other):
return mycmp(self.obj, other.obj) != 0
return K
def _par_cmp(obj1, obj2):
"""
Original par_cmp
"""
assert(isinstance(obj1, tuple))
assert(isinstance(obj2, tuple))
assert(isinstance(obj1[0], str))
assert(isinstance(obj2[0], str))
if isinstance(obj1[1], ParameterDict) and \
not isinstance(obj2[1], ParameterDict):
return -1
if not isinstance(obj1[1], ParameterDict) and \
isinstance(obj2[1], ParameterDict):
return 1
return cmp(obj1[0], obj2[0])
par_cmp = cmp_to_key(_par_cmp)
[docs]class ParameterDict(dict):
"""
A dictionary with attribute-style access,
that maps attribute access to the real dictionary.
"""
__slots__ = ()
def __new__(cls, **params):
# Generate a sub class of ParameterDict which sets the slots.
# This is nice so the parameters show up in IPython tab completion
class SubParameterDict(ParameterDict):
__slots__ = tuple(list(params.keys())+["_members"])
return dict.__new__(SubParameterDict, **params)
def __init__(self, **params):
"""
Initialize a ParameterDict
Arguments
---------
params : (kwargs)
A kwargs dict of Parameters of other ParameterDicts
"""
# Init the dict with the provided parameters
self._members = sorted(set(list(dict.__dict__) + \
list(ParameterDict.__dict__)))+["_members"]
for key, value in list(params.items()):
if key in self._members:
type_error("The name of a parameter cannot be "\
"an attribute of 'ParameterDict': %s" % key)
if sp and isinstance(value, sp.Basic) and \
all(isinstance(atom, (sp.Number, sp.Symbol))
for atom in value.atoms()):
params[key] = SlaveParam(value, key, name=key)
elif isinstance(value, Param):
if value.name == "":
value.name = key
elif not isinstance(value, ParameterDict):
params[key] = Param(value, key)
self._members.append(key)
# Initialize base class
dict.__init__(self, **params)
def __getstate__(self):
return list(self.__dict__.items())
def __setstate__(self, items):
for key, val in items:
self.__setattr__(key, val)
def __str__(self):
"""
Returns a nice representation of the ParameterDict
"""
return self.format_data()
def __repr__(self):
return "ParameterDict(%s)"%(", ".join("%s=%s" %\
(k, repr(v)) for k, v in sorted(six.iteritems(self), key=par_cmp)))
def __delitem__(self, key):
type_error("ParameterDict does not support item deletion")
[docs] def pop(self, key):
type_error("ParameterDict does not support item removal")
[docs] def clear(self):
type_error("ParameterDict does not support item removal")
[docs] def fromkeys(self, *args):
type_error("ParameterDict does not support fromkeys")
def __setattr__(self, key, value):
check_arg(key, str, 0, ParameterDict.__setattr__)
if key == "_members":
dict.__setattr__(self, key, value)
return
# Check if key is a registered parameter
if not dict.__contains__(self, key):
error("'%s' is not an item in this ParameterDict." % key, \
exception=AttributeError)
# Get the original value, used for checks
org_value = dict.__getitem__(self, key)
if isinstance(org_value, ParameterDict):
type_error("cannot overwrite a ParameterDict")
# Set the new value
if isinstance(org_value, Param):
org_value.setvalue(value)
else:
dict.__setitem__(self, key, value)
def __getattr__(self, key):
check_arg(key, str, 0, ParameterDict.__getattr__)
# Fix for newer ipython
if key in ["__dict__", "__methods__", "trait_names", "_getAttributeNames"]:
return
if not dict.__contains__(self, key):
error("'%s' is not an item in this ParameterDict." % key, \
exception=AttributeError)
value = dict.__getitem__(self, key)
if isinstance(value, Param):
value = value.getvalue()
return value
def __setitem__(self, key, value):
self.__setattr__(key, value)
def __getitem__(self, key):
return self.__getattr__(key)
def __members__(self):
return self._members
[docs] def iterparams(self, recurse=False):
"""
Iterate over all Param
Arguments
---------
recurse : bool (optional)
If True each encountered ParameterDict will be entered
"""
for key, value in sorted(six.iteritems(self), key=par_cmp):
if isinstance(value, ParameterDict) and recurse:
for new_value in value.iterparams(recurse):
yield new_value
elif isinstance(value, Param):
yield value
[docs] def iterparameterdicts(self):
"""
Iterate over all ParameterDicts
Arguments
---------
recurse : bool (optional)
If True each encountered ParameterDict will also be entered
"""
for key, value in sorted(six.iteritems(self), key=par_cmp):
if isinstance(value, ParameterDict):
yield value
# A nice string to use '\xe2\x88\x88'= \in and '\xe2\x88\x9e'= \infty
[docs] def copy(self, to_dict=False):
"""
Make a deep copy of self, including recursive copying of parameter
subsets.
Arguments
---------
to_dict : bool (optional)
Return a dict with items representing the values of the
Parameters
"""
items = {}
for key in six.iterkeys(self):
value = dict.__getitem__(self, key)
# If the value is a ParameterDict
if isinstance(value, ParameterDict):
items[key] = value.copy(to_dict)
else:
if to_dict and isinstance(value, Param):
items[key] = value.getvalue()
elif isinstance(value, SlaveParam):
items[key] = value
else:
items[key] = eval(repr(value))
# FIXME: Why is this nessesary?
items.pop("__builtins__", None)
if to_dict:
ch = dict(**items)
else:
ch = ParameterDict(**items)
return ch
[docs] def update(self, other):
"""
A recursive update that handles parameter subsets
correctly unlike dict.update.
"""
check_arg(other, dict, 0, ParameterDict.update)
for key in six.iterkeys(other):
if key not in self:
continue
self_value = self[key]
other_value = other[key]
if isinstance(self_value, dict):
# Update my own subdict with others subdict
self_value.update(other_value)
elif isinstance(self_value, Param):
# Set my own value to others value
self_value.setvalue(other_value)
else:
self[key] = other_value
[docs] def parse_args(self, options=None, usage=""):
"""
Parse a list of options. use sys.argv as default
Arguments
---------
options : list of str (optional)
List of options. By default sys.argv[1:] is used. This argument
is mostly for debugging.
"""
import optparse, sys
# Fixing bug for unicode help output
class OptionParser(optparse.OptionParser):
def print_help(self, f=None):
if f is None:
f = sys.stdout
f.write(self.format_help())
parser = OptionParser(usage = usage or "usage: %prog [options]")
def callback(parent, key, value_type, sequence_type=None):
" Return a callback function that is used to parse the argument"
if value_type in [int, float, str, bool]:
def par_setter(option, opt_str, value, parser):
" Callback function to set the parameter from the options."
try:
parent[key] = value
#debug("Setting parameter %s to %s"%\
# (opt_str.replace(PAR_PREFIX, ""), str(value)))
except ValueError as e:
value_error("Trying to set '%s' while parsing "\
"command line, but %s" % (key, six.text_type(e)))
else:
def par_setter(option, opt_str, value, parser):
assert value is None
done = 0
value = []
rargs = parser.rargs
while rargs:
arg = rargs[0]
# Stop if we hit an arg like "--par", i.e, PAR_PREFIX
if PAR_PREFIX in arg:
break
else:
try:
# Convert the value
item = sequence_type(arg)
value.append(item)
except ValueError as e:
value_error(\
"Could not convert %s to '%s', while "\
"setting parameter %s; %s"%\
(arg, sequence_type.__name__, key,
six.text_type(e)))
del rargs[0]
try:
# Changing a list to a tuple if needed
parent[key] = value_type(value)
#debug("Setting parameter %s to %s"%\
#(opt_str.replace(PAR_PREFIX, ""), str(value)))
except ValueError as e:
value_error("Trying to set '%s' while parsing "\
"command line, but %s" % (key,
six.text_type(e)))
return par_setter
def add_options(parent, opt_base):
for key, value in sorted(six.iteritems(parent), key=par_cmp):
opt_base_copy = opt_base[:]
if opt_base != PAR_PREFIX:
opt_base_copy += "."
# If the value is a ParameterDict
if isinstance(value, ParameterDict):
# Call the function recursively
add_options(value, "%s%s"%(opt_base_copy, key))
continue
elif isinstance(value, Param):
# ConstParam, ArrayParam cannot be parsed, yet
if isinstance(value, (ArrayParam, ConstParam)):
continue
# If the value is a Param get the value
actuall_value = value.getvalue()
# Get description
description = value.description
# Check for sequence
if isinstance(actuall_value, (list, tuple)):
# If a default length of the list or tuple is 0,
# assume sequence type to be int
if len(actuall_value) == 0:
sequence_type = int
else:
# Else assume it to be equal to the first argument
sequence_type = type(actuall_value[0])
else:
sequence_type = None
# Nicely formated value
formated_value = value.format_data()
# Check for available types
if not type(actuall_value) in list(FORMAT_CONVERTER.keys()):
continue
# Add option with callback function
parser.add_option("%s%s"%(opt_base_copy, key), \
action = "callback",
callback = callback(\
parent, key, type(actuall_value), sequence_type),
type = FORMAT_CONVERTER[type(actuall_value)],
help = "Default(%s)%s" % (str(formated_value),
(": " + description) if description else ""))
# Start recursively adding options
add_options(self, PAR_PREFIX)
# Parse command line options
if options:
parser.parse_args(options)
else:
parser.parse_args()
[docs] def optstr(self):
"""
Return a string with option set
An option string can be sent to a script using a parameter dict
to set its parameters from command line options
"""
def option_list(parent, opt_base):
ret_list = []
opt_base_copy = opt_base[:]
if opt_base != PAR_PREFIX:
opt_base_copy += "."
for key, value in sorted(six.iteritems(parent), key=par_cmp):
# If the value is a ParameterDict
if isinstance(value, ParameterDict):
# Call the function recursively
ret_list.extend(\
option_list(value, "%s%s"%(opt_base_copy, key)))
elif isinstance(value, Param):
if isinstance(value, (ConstParam, SlaveParam)):
continue
# If the value is a Param get the value
value = value.getvalue()
# Check for available types
if not type(value) in list(FORMAT_CONVERTER.keys()):
continue
ret_list.append("%s%s"%(opt_base_copy, key))
if type(value) in [list, tuple]:
for item in value:
ret_list.append(six.text_type(item))
else:
value = int(value) if isinstance(value, bool) else value
ret_list.append(six.text_type(value))
return ret_list
return " " + " ".join(option_list(self, PAR_PREFIX))