# -*- coding: utf-8 -*-
"""
FIXME:
This class is very old, convoluted, and coupled.
It really needs to be rewritten efficiently.
the __setattr__ __getattr__ stuff needs to be redone, and
DynStruct probably needs to go away.
"""
from __future__ import absolute_import, division, print_function, unicode_literals
import six
from six.moves import cPickle as pickle
try:
import numpy as np
except ImportError as ex:
pass
from utool import DynamicStruct
from utool import util_dbg
from utool import util_arg
from utool import util_type
from utool import util_inject
# print, rrr, profile = util_inject.inject(__name__, '[pref]')
util_inject.noinject(__name__, '[pref]')
# ---
# GLOBALS
# ---
PrefNode = DynamicStruct.DynStruct
VERBOSE_PREF = util_arg.get_argflag('--verbpref')
# ---
# Classes
# ---
[docs]class PrefInternal(DynamicStruct.DynStruct):
def __init__(_intern, name, doc, default, hidden, fpath, depeq, choices):
super(PrefInternal, _intern).__init__(child_exclude_list=[])
# self._intern describes this node
_intern.name = name # A node has a name
_intern.doc = doc # A node has a name
_intern.value = default # A node has a value
_intern.hidden = hidden # A node can be hidden
_intern.fpath = fpath # A node is cached to
_intern.depeq = depeq # A node depends on
_intern.editable = True # A node can be uneditable
_intern._frozen_type = None # A node's value type
# Some preferences are constrained to a list of choices
if choices is not None:
_intern.value = PrefChoice(choices, default)
[docs] def get_type(_intern):
if _intern._frozen_type is not None:
return _intern._frozen_type
else:
return type(_intern.value)
[docs] def freeze_type(_intern):
_intern._frozen_type = _intern.get_type()
[docs]class PrefTree(DynamicStruct.DynStruct):
def __init__(_tree, parent):
super(PrefTree, _tree).__init__(child_exclude_list=[])
# self._tree describes node's children and the parents
# relationship to them
_tree.parent = parent # Children have parents
_tree.hidden_children = [] # Children can be hidden
_tree.child_list = [] # There can be many children
_tree.child_names = [] # Each child has a name
_tree.num_visible_children = 0 # Statistic
_tree.aschildx = 0 # This node is the x-th child
[docs]class PrefChoice(DynamicStruct.DynStruct):
def __init__(self, choices, default):
super(PrefChoice, self).__init__(child_exclude_list=[])
self.choices = choices
self.sel = 0
self.change_val(default)
[docs] def change_val(self, new_val):
# Try to select by index
if isinstance(new_val, int):
self.sel = new_val
# Try to select by value
elif isinstance(new_val, six.string_types):
self.sel = self.choices.index(new_val)
else:
raise ('Exception: Unknown newval=%r' % new_val)
if self.sel < 0 or self.sel > len(self.choices):
raise Exception(
'self.sel=%r is not in the self.choices=%r ' % (self.sel, self.choices)
)
[docs] def combo_val(self):
return self.choices[self.sel]
[docs] def get_tuple(self):
return (self.sel, self.choices)
[docs]class Pref(PrefNode):
"""
Structure for Creating Preferences.
Caveats: When using a value call with ['valname'] to be safe
Features:
* Can be saved and loaded.
* Can be nested
* Dynamically add/remove
"""
def __init__(
self,
default=PrefNode, # Default value for a Pref is to be itself
doc='empty docs', # Documentation for a preference
hidden=False, # Is a hidden preference?
choices=None, # A list of choices
depeq=None, # List of tuples representing dependencies
fpath='', # Where to save to
name='root', # Name of this node
parent=None,
): # Reference to parent Pref
"""
Creates a pref struct that will save itself to pref_fpath if
available and have init all members of some dictionary
"""
super(Pref, self).__init__(child_exclude_list=['_intern', '_tree'])
# Private internal structure
self._intern = PrefInternal(name, doc, default, hidden, fpath, depeq, choices)
self._tree = PrefTree(parent)
# if default is PrefNode:
# print('----------')
# print('new Pref(default=PrefNode)')
[docs] def get_type(self):
return self._intern.get_type()
[docs] def ensure_attr(self, attr, default):
if not hasattr(self, attr):
setattr(self, attr, default)
return getattr(self, attr)
# -------------------
# Attribute Setters
[docs] def toggle(self, key):
""" Toggles a boolean key """
val = self[key]
assert isinstance(val, bool), 'key[%r] = %r is not a bool' % (key, val)
self.pref_update(key, not val)
[docs] def change_combo_val(self, new_val):
"""
Checks to see if a selection is a valid index or choice of a combo
preference
"""
choice_obj = self._intern.value
assert isinstance(self._intern.value, PrefChoice), 'must be a choice'
return choice_obj.get_tuple()
def __overwrite_child_attr(self, name, attr):
# FIXME: when setting string preference nodes to lists, it casts
# the list to a string!
if VERBOSE_PREF:
print(
'[pref.__overwrite_child_attr]: %s.%s = %r'
% (self._intern.name, name, attr)
)
# get child node to "overwrite"
row = self._tree.child_names.index(name)
child = self._tree.child_list[row]
if isinstance(attr, Pref):
# Do not break pointers when overwriting a Preference
if issubclass(attr._intern.value, PrefNode):
# Main Branch Logic
for (key, val) in six.iteritems(attr):
child.__setattr__(key, val)
else:
self.__overwrite_child_attr(name, attr.value())
else: # Main Leaf Logic:
# assert(not issubclass(child._intern.type, PrefNode), #(self.full_name() + ' Must be a leaf'))
# Keep user-readonly map up to date with internals
if isinstance(child._intern.value, PrefChoice):
child.change_combo_val(attr)
else:
child_type = child._intern.get_type()
if isinstance(attr, six.string_types) and issubclass(
child_type, six.string_types
):
# import utool as ut
# ut.embed()
attr = child_type(attr)
attr_type = type(attr)
if attr is not None and child_type is not attr_type:
if util_arg.VERBOSE:
print('[pref] WARNING TYPE DIFFERENCE!')
print('[pref] * expected child_type = %r' % (child_type,))
print('[pref] * got attr_type = %r' % (attr_type,))
print('[pref] * name = %r' % (name,))
print('[pref] * attr = %r' % (attr,))
attr = util_type.try_cast(attr, child_type, attr)
child._intern.value = attr
self.__dict__[name] = child.value()
def __new_attr(self, name, attr):
"""
On a new child attribute:
1) Check to see if it is wrapped by a Pref object
2) If not do so, if so add it to the tree structure
"""
if isinstance(attr, Pref):
# Child attribute already has a Pref wrapping
if VERBOSE_PREF:
print(
'[pref.__new_attr]: %s.%s = %r'
% (self._intern.name, name, attr.value())
)
new_childx = len(self._tree.child_names)
# Children know about parents
attr._tree.parent = self # Give child parent
attr._intern.name = name # Give child name
if attr._intern.depeq is None:
attr._intern.depeq = self._intern.depeq # Give child parent dependencies
if attr._intern.hidden:
self._tree.hidden_children.append(new_childx)
self._tree.hidden_children.sort()
# Used for QTIndexing
attr._intern.aschildx = new_childx
# Parents know about children
self._tree.child_names.append(name) # Add child to tree
self._tree.child_list.append(attr)
self.__dict__[name] = attr.value() # Add child value to dict
else:
# The child attribute is not wrapped. Wrap with Pref and readd.
pref_attr = Pref(default=attr)
self.__new_attr(name, pref_attr)
# Attributes are children
def __setattr__(self, name, attr):
"""
Called when an attribute assignment is attempted. This is called instead
of the normal mechanism (i.e. store the value in the instance
dictionary). name is the attribute name, value is the value to be
assigned to it.
If __setattr__() wants to assign to an instance attribute, it should not
simply execute self.name = value this would cause a recursive call to
itself. Instead, it should insert the value in the dictionary of
instance attributes, e.g., self.__dict__[name] = value. For new-style
classes, rather than accessing the instance dictionary, it should call
the base class method with the same name, for example,
object.__setattr__(self, name, value).
'Wraps child attributes in a Pref object if not already done'
"""
# No wrapping for private vars: _printable_exclude, _intern, _tree
if name.find('_') == 0:
return super(DynamicStruct.DynStruct, self).__setattr__(name, attr)
# Overwrite if child exists
if name in self._tree.child_names:
self.__overwrite_child_attr(name, attr)
else:
self.__new_attr(name, attr)
# -------------------
# Attribute Getters
[docs] def value(self):
# Return the wrapper in all its glory
if self._intern.value == PrefNode:
return self
# Return basic types
elif isinstance(self._intern.value, PrefChoice):
return self._intern.value.combo_val()
else:
return self._intern.value # TODO AS REFERENCE
def __getattr__(self, name):
"""
Called when an attribute lookup has not found the attribute in the usual
places
(i.e. it is not an instance attribute nor is it found in the class tree
for self).
name is the attribute name.
This method should return the (computed) attribute value or raise an
AttributeError exception.
Get a child from this parent called as last resort.
Allows easy access to internal prefs
"""
if name.find('_') == 0:
# Names that start with underscore actually belong to the Pref object
return super(PrefNode, self).__getitem__[name]
if len(name) > 9 and name[-9:] == '_internal':
# internal names belong to the internal structure
attrx = self._tree.child_names.index(name[:-9])
return self._tree.child_list[attrx]
# print(self._internal.name)
# print(self._tree)
try:
if six.PY2:
return super(PrefNode, self).__getitem__[name]
else:
try:
base_self1 = super(PrefNode, self)
# base_self2 = super(DynStruct, self)
# base_self3 = super()
# import utool
# utool.embed()
return base_self1[name]
except Exception as ex:
print(ex)
print('base_self1 = %r' % (base_self1,))
# print('base_self2 = %r' % (base_self2,))
# print('base_self3 = %r' % (base_self3,))
print('name = %r' % (name,))
raise
except Exception as ex:
if name == 'trait_names':
# HACK FOR IPYTHON
pass
else:
import utool as ut
if ut.DEBUG2 and ut.VERBOSE:
ut.printex(
ex,
'Pref object missing named attribute',
keys=['self._intern.name', 'name'],
iswarning=True,
)
raise AttributeError(
(
'Pref object is missing named attribute: name=%r.'
'You might try running wbia with --nocache-pref '
'to see if that fixes things.'
)
% name
)
# raise
[docs] def iteritems(self):
"""
Wow this class is messed up. I had to overwrite items when
moving to python3, just because I haden't called it yet
"""
for (key, val) in six.iteritems(self.__dict__):
if key in self._printable_exclude:
continue
yield (key, val)
[docs] def items(self):
"""
Wow this class is messed up. I had to overwrite items when
moving to python3, just because I haden't called it yet
"""
for (key, val) in six.iteritems(self.__dict__):
if key in self._printable_exclude:
continue
yield (key, val)
# ----------------
# Disk caching
[docs] def to_dict(self, split_structs_bit=False):
""" Converts prefeters to a dictionary.
Children Pref can be optionally separated """
pref_dict = {}
struct_dict = {}
for (key, val) in six.iteritems(self):
if split_structs_bit and isinstance(val, Pref):
struct_dict[key] = val
continue
pref_dict[key] = val
if split_structs_bit:
return (pref_dict, struct_dict)
return pref_dict
asdict = to_dict
[docs] def save(self):
""" Saves prefs to disk in dict format """
fpath = self.get_fpath()
if fpath in ['', None]:
if self._tree.parent is not None:
if VERBOSE_PREF:
print('[pref.save] Can my parent save me?') # ...to disk
return self._tree.parent.save()
if VERBOSE_PREF:
print('[pref.save] I cannot be saved. I have no parents.')
return False
with open(fpath, 'wb') as f:
print('[pref] Saving to ' + fpath)
pref_dict = self.to_dict()
pickle.dump(
pref_dict, f, protocol=2
) # Use protocol 2 to support python2 and 3
return True
[docs] def get_fpath(self):
return self._intern.fpath
[docs] def load(self):
""" Read pref dict stored on disk. Overwriting current values. """
if VERBOSE_PREF:
print('[pref.load()]')
# if not os.path.exists(self._intern.fpath):
# msg = '[pref] fpath=%r does not exist' % (self._intern.fpath)
# return msg
fpath = self.get_fpath()
try:
with open(fpath, 'rb') as f:
if VERBOSE_PREF:
print('load: %r' % fpath)
pref_dict = pickle.load(f)
except EOFError as ex1:
util_dbg.printex(
ex1, 'did not load pref fpath=%r correctly' % fpath, iswarning=True
)
# warnings.warn(msg)
raise
# return msg
except ImportError as ex2:
util_dbg.printex(
ex2, 'did not load pref fpath=%r correctly' % fpath, iswarning=True
)
# warnings.warn(msg)
raise
# return msg
if not util_type.is_dict(pref_dict):
raise Exception('Preference file is corrupted')
self.add_dict(pref_dict)
return True
# ----------------------
# String representation
def __str__(self):
if self._intern.value != PrefNode:
ret = super(PrefNode, self).__str__()
# .replace('\n ', '')
ret += '\nLEAF ' + repr(self._intern.name) + ':' + repr(self._intern.value)
return ret
else:
ret = repr(self._intern.value)
return ret
[docs] def full_name(self):
""" returns name all the way up the tree """
if self._tree.parent is None:
return self._intern.name
return self._tree.parent.full_name() + '.' + self._intern.name
[docs] def get_printable(self, type_bit=True, print_exclude_aug=[]):
# Remove unsatisfied dependencies from the printed structure
further_aug = print_exclude_aug[:]
for child_name in self._tree.child_names:
depeq = self[child_name + '_internal']._intern.depeq
if depeq is not None and depeq[0].value() != depeq[1]:
further_aug.append(child_name)
return super(Pref, self).get_printable(type_bit, print_exclude_aug=further_aug)
[docs] def customPrintableType(self, name):
if name in self._tree.child_names:
row = self._tree.child_names.index(name)
# child = self._tree.child_list[row] # child node to "overwrite"
_typestr = type(self._tree.child_list[row]._intern.value)
if util_type.is_str(_typestr):
return _typestr
[docs] def pref_update(self, key, new_val):
""" Changes a preference value and saves it to disk """
print(
'Update and save pref from: %s=%r, to: %s=%r'
% (key, six.text_type(self[key]), key, six.text_type(new_val))
)
self.__setattr__(key, new_val)
return self.save()
[docs] def update(self, **kwargs):
# print('Updating Preference: kwargs = %r' % (kwargs))
self_keys = set(self.__dict__.keys())
for key, val in six.iteritems(kwargs):
if key in self_keys:
self.__setattr__(key, val)
# Method for QTWidget
[docs] def qt_get_parent(self):
return self._tree.parent
[docs] def qt_parents_index_of_me(self):
return self._tree.aschildx
[docs] def qt_get_child(self, row):
row_offset = (np.array(self._tree.hidden_children) <= row).sum()
return self._tree.child_list[row + row_offset]
[docs] def qt_row_count(self):
return len(self._tree.child_list) - len(self._tree.hidden_children)
[docs] def qt_col_count(self):
return 2
[docs] def qt_get_data(self, column):
if column == 0:
return self._intern.name
data = self.value()
if isinstance(data, Pref): # Recursive Case: Pref
data = ''
elif data is None:
# Check for a get of None
data = 'None'
return data
[docs] def qt_is_editable(self):
uneditable_hack = ['feat_type']
if self._intern.name in uneditable_hack:
return False
if self._intern.depeq is not None:
return self._intern.depeq[0].value() == self._intern.depeq[1]
# return self._intern.value is not None
return self._intern.value != PrefNode
[docs] def qt_set_leaf_data(self, qvar):
return _qt_set_leaf_data(self, qvar)
def _qt_set_leaf_data(self, qvar):
""" Sets backend data using QVariants """
if VERBOSE_PREF:
print('')
print('+--- [pref.qt_set_leaf_data]')
print('[pref.qt_set_leaf_data] qvar = %r' % qvar)
print('[pref.qt_set_leaf_data] _intern.name=%r' % self._intern.name)
print('[pref.qt_set_leaf_data] _intern.type_=%r' % self._intern.get_type())
print('[pref.qt_set_leaf_data] type(_intern.value)=%r' % type(self._intern.value))
print('[pref.qt_set_leaf_data] _intern.value=%r' % self._intern.value)
# print('[pref.qt_set_leaf_data] qvar.toString()=%s' % six.text_type(qvar.toString()))
if self._tree.parent is None:
raise Exception('[Pref.qtleaf] Cannot set root preference')
if self.qt_is_editable():
new_val = '[Pref.qtleaf] BadThingsHappenedInPref'
if self._intern.value == PrefNode:
raise Exception('[Pref.qtleaf] Qt can only change leafs')
elif self._intern.value is None:
# None could be a number of types
def cast_order(var, order=[bool, int, float, six.text_type]):
for type_ in order:
try:
ret = type_(var)
return ret
except Exception:
continue
new_val = cast_order(six.text_type(qvar))
self._intern.get_type()
if isinstance(self._intern.value, bool):
# new_val = bool(qvar.toBool())
print('qvar = %r' % (qvar,))
new_val = util_type.smart_cast(qvar, bool)
# new_val = bool(eval(qvar, {}, {}))
print('new_val = %r' % (new_val,))
elif isinstance(self._intern.value, int):
# new_val = int(qvar.toInt()[0])
new_val = int(qvar)
# elif isinstance(self._intern.value, float):
elif self._intern.get_type() in util_type.VALID_FLOAT_TYPES:
# new_val = float(qvar.toDouble()[0])
new_val = float(qvar)
elif isinstance(self._intern.value, six.string_types):
# new_val = six.text_type(qvar.toString())
new_val = six.text_type(qvar)
elif isinstance(self._intern.value, PrefChoice):
# new_val = qvar.toString()
new_val = six.text_type(qvar)
if new_val.upper() == 'NONE':
new_val = None
else:
try:
# new_val = six.text_type(qvar.toString())
type_ = self._intern.get_type()
if type_ is not None:
new_val = type_(six.text_type(qvar))
else:
new_val = six.text_type(qvar)
except Exception:
raise NotImplementedError(
(
'[Pref.qtleaf] Unknown internal type. '
'type(_intern.value) = %r, '
'_intern.get_type() = %r, '
)
% type(self._intern.value),
self._intern.get_type(),
)
# Check for a set of None
if isinstance(new_val, six.string_types):
if new_val.lower() == 'none':
new_val = None
elif new_val.lower() == 'true':
new_val = True
elif new_val.lower() == 'false':
new_val = False
# save to disk after modifying data
if VERBOSE_PREF:
print('---')
print('[pref.qt_set_leaf_data] new_val=%r' % new_val)
print('[pref.qt_set_leaf_data] type(new_val)=%r' % type(new_val))
print('L____ [pref.qt_set_leaf_data]')
# TODO Add ability to set a callback function when certain
# preferences are changed.
return self._tree.parent.pref_update(self._intern.name, new_val)
return 'PrefNotEditable'
[docs]def test_Preferences():
r"""
CommandLine:
python -m utool.Preferences --test-test_Preferences --show --verbpref
Example:
>>> # DISABLE_DOCTEST
>>> # xdoctest: +REQUIRES(module:wbia.guitool)
>>> # FIXME depends on guitool_ibei
>>> from utool.Preferences import * # NOQA
>>> import utool as ut
>>> import wbia.guitool
>>> wbia.guitool.ensure_qtapp()
>>> root = test_Preferences()
>>> ut.quit_if_noshow()
>>> widget = root.createQWidget()
>>> #widget.show()
>>> wbia.guitool.qtapp_loop(widget)
"""
root = Pref()
root.a = Pref()
root.strvar = 'foobar'
root.intvar = 1
root.floatvar = -1.3
root.boolvar = True
root.listvar = [True, 1, 3]
root.a.strvar = 'foobar1'
root.a.intvar = -1
root.a.floatvar = 2.4
root.a.boolvar2 = False
return root
if __name__ == '__main__':
"""
CommandLine:
python -m utool.Preferences
python -m utool.Preferences --allexamples
python -m utool.Preferences --allexamples --noface --nosrc
"""
import multiprocessing
multiprocessing.freeze_support() # for win32
import utool as ut # NOQA
ut.doctest_funcs()