Source code for apptools.preferences.preferences
# (C) Copyright 2005-2024 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!
""" The default implementation of a node in a preferences hierarchy. """
# Standard library imports.
import logging
import threading
# Enthought library imports.
from traits.api import Any, Callable, Dict, HasTraits, Instance, List
from traits.api import Property, Str, Undefined, provides
# Local imports.
from .i_preferences import IPreferences
# Logging.
logger = logging.getLogger(__name__)
[docs]@provides(IPreferences)
class Preferences(HasTraits):
""" The default implementation of a node in a preferences hierarchy. """
#### 'IPreferences' interface #############################################
# The absolute path to this node from the root node (the empty string if
# this node *is* the root node).
path = Property(Str)
# The parent node (None if this node *is* the root node).
parent = Instance(IPreferences)
# The name of the node relative to its parent (the empty string if this
# node *is* the root node).
name = Str
#### 'Preferences' interface ##############################################
# The default name of the file used to persist the preferences (if no
# filename is passed in to the 'load' and 'save' methods, then this is
# used instead).
filename = Str
#### Protected 'Preferences' interface ####################################
# A lock to make access to the node thread-safe.
#
# fixme: There *should* be no need to declare this as a trait, but if we
# don't then we have problems using nodes in the preferences manager UI.
# It is something to do with 'cloning' the node for use in a 'modal' traits
# UI... Hmmm...
_lk = Any
# The node's children.
_children = Dict(Str, IPreferences)
# The node's preferences.
_preferences = Dict(Str, Any)
# Listeners for changes to the node's preferences.
#
# The callable must take 4 arguments, e.g::
#
# listener(node, key, old, new)
_preferences_listeners = List(Callable)
###########################################################################
# 'object' interface.
###########################################################################
def __init__(self, **traits):
""" Constructor. """
# A lock to make access to the '_children', '_preferences' and
# '_preferences_listeners' traits thread-safe.
self._lk = threading.Lock()
# Base class constructor.
super().__init__(**traits)
# If a filename has been specified then load the preferences from it.
if len(self.filename) > 0:
self.load()
###########################################################################
# 'IPreferences' interface.
###########################################################################
#### Trait properties #####################################################
def _get_path(self):
""" Property getter. """
names = []
node = self
while node.parent is not None:
names.append(node.name)
node = node.parent
names.reverse()
return ".".join(names)
#### Methods ##############################################################
#### Methods where 'path' refers to a preference ####
[docs] def get(self, path, default=None, inherit=False):
""" Get the value of the preference at the specified path. """
if len(path) == 0:
raise ValueError("empty path")
components = path.split(".")
# If there is only one component in the path then the operation takes
# place in this node.
if len(components) == 1:
value = self._get(path, Undefined)
# Otherwise, find the next node and pass the rest of the path to that.
else:
node = self._get_child(components[0])
if node is not None:
value = node.get(".".join(components[1:]), Undefined)
else:
value = Undefined
# If inherited values are allowed then try those as well.
#
# e.g. 'acme.ui.widget.bgcolor'
# 'acme.ui.bgcolor'
# 'acme.bgcolor'
# 'bgcolor'
while inherit and value is Undefined and len(components) > 1:
# Remove the penultimate component...
#
# e.g. 'acme.ui.widget.bgcolor' -> 'acme.ui.bgcolor'
del components[-2]
# ... and try that.
value = self.get(".".join(components), default=Undefined)
if value is Undefined:
value = default
return value
[docs] def remove(self, path):
""" Remove the preference at the specified path. """
if len(path) == 0:
raise ValueError("empty path")
components = path.split(".")
# If there is only one component in the path then the operation takes
# place in this node.
if len(components) == 1:
self._remove(path)
# Otherwise, find the next node and pass the rest of the path to that.
else:
node = self._get_child(components[0])
if node is not None:
node.remove(".".join(components[1:]))
[docs] def set(self, path, value):
""" Set the value of the preference at the specified path. """
if len(path) == 0:
raise ValueError("empty path")
components = path.split(".")
# If there is only one component in the path then the operation takes
# place in this node.
if len(components) == 1:
self._set(path, value)
# Otherwise, find the next node (creating it if it doesn't exist)
# and pass the rest of the path to that.
else:
node = self._node(components[0])
node.set(".".join(components[1:]), value)
#### Methods where 'path' refers to a node ####
[docs] def clear(self, path=""):
""" Remove all preferences from the node at the specified path. """
# If the path is empty then the operation takes place in this node.
if len(path) == 0:
self._clear()
# Otherwise, find the next node and pass the rest of the path to that.
else:
components = path.split(".")
node = self._get_child(components[0])
if node is not None:
node.clear(".".join(components[1:]))
[docs] def keys(self, path=""):
""" Return the preference keys of the node at the specified path. """
# If the path is empty then the operation takes place in this node.
if len(path) == 0:
keys = self._keys()
# Otherwise, find the next node and pass the rest of the path to that.
else:
components = path.split(".")
node = self._get_child(components[0])
if node is not None:
keys = node.keys(".".join(components[1:]))
else:
keys = []
return keys
[docs] def node(self, path=""):
""" Return the node at the specified path. """
# If the path is empty then the operation takes place in this node.
if len(path) == 0:
node = self
# Otherwise, find the next node and pass the rest of the path to that.
else:
components = path.split(".")
node = self._node(components[0])
node = node.node(".".join(components[1:]))
return node
[docs] def node_exists(self, path=""):
""" Return True if the node at the specified path exists. """
# If the path is empty then the operation takes place in this node.
if len(path) == 0:
exists = True
# Otherwise, find the next node and pass the rest of the path to that.
else:
components = path.split(".")
node = self._get_child(components[0])
if node is not None:
exists = node.node_exists(".".join(components[1:]))
else:
exists = False
return exists
[docs] def node_names(self, path=""):
"""Return the names of the children of the node at the specified path.
"""
# If the path is empty then the operation takes place in this node.
if len(path) == 0:
names = self._node_names()
# Otherwise, find the next node and pass the rest of the path to that.
else:
components = path.split(".")
node = self._get_child(components[0])
if node is not None:
names = node.node_names(".".join(components[1:]))
else:
names = []
return names
#### Persistence methods ####
[docs] def flush(self):
"""Force any changes in the node to the backing store.
This includes any changes to the node's descendants.
"""
self.save()
###########################################################################
# 'Preferences' interface.
###########################################################################
#### Listener methods ####
[docs] def add_preferences_listener(self, listener, path=""):
""" Add a listener for changes to a node's preferences. """
# If the path is empty then the operation takes place in this node.
if len(path) == 0:
self._add_preferences_listener(listener)
# Otherwise, find the next node and pass the rest of the path to that.
else:
components = path.split(".")
node = self._node(components[0])
node.add_preferences_listener(listener, ".".join(components[1:]))
[docs] def remove_preferences_listener(self, listener, path=""):
""" Remove a listener for changes to a node's preferences. """
# If the path is empty then the operation takes place in this node.
if len(path) == 0:
self._remove_preferences_listener(listener)
# Otherwise, find the next node and pass the rest of the path to that.
else:
components = path.split(".")
node = self._node(components[0])
node.remove_preferences_listener(
listener, ".".join(components[1:])
)
#### Persistence methods ####
[docs] def load(self, file_or_filename=None):
"""Load preferences from a file.
This is a *merge* operation i.e. the contents of the file are added to
the node.
This implementation uses 'ConfigObj' files.
"""
if file_or_filename is None:
file_or_filename = self.filename
logger.debug("loading preferences from <%s>", file_or_filename)
# Do the import here so that we don't make 'ConfigObj' a requirement
# if preferences aren't ever persisted (or a derived class chooses to
# use a different persistence mechanism).
from configobj import ConfigObj
config_obj = ConfigObj(file_or_filename, encoding="utf-8")
# 'name' is the section name, 'value' is a dictionary containing the
# name/value pairs in the section (the actual preferences ;^).
for name, value in config_obj.items():
# Create/get the node from the section name.
components = name.split(".")
node = self
for component in components:
node = node._node(component)
# Add the contents of the section to the node.
self._add_dictionary_to_node(node, value)
[docs] def save(self, file_or_filename=None):
"""Save the node's preferences to a file.
This implementation uses 'ConfigObj' files.
"""
if file_or_filename is None:
file_or_filename = self.filename
# If no file or filename is specified then don't save the preferences!
if len(file_or_filename) > 0:
# Do the import here so that we don't make 'ConfigObj' a
# requirement if preferences aren't ever persisted (or a derived
# class chooses to use a different persistence mechanism).
from configobj import ConfigObj
logger.debug("saving preferences to <%s>", file_or_filename)
config_obj = ConfigObj(file_or_filename, encoding="utf-8")
self._add_node_to_dictionary(self, config_obj)
config_obj.write()
###########################################################################
# Protected 'Preferences' interface.
#
# These are the only methods that should access the protected '_children'
# and '_preferences' traits. This helps make it easy to subclass this class
# to create other implementations (all the subclass has to do is to
# implement these protected methods).
#
###########################################################################
def _add_dictionary_to_node(self, node, dictionary):
""" Add the contents of a dictionary to a node's preferences. """
with self._lk:
node._preferences.update(dictionary)
def _add_node_to_dictionary(self, node, dictionary):
""" Add a node's preferences to a dictionary. """
# This method never manipulates the '_preferences' trait directly.
# Instead it does eveything via the other protected methods and hence
# doesn't need to grab the lock.
if len(node._keys()) > 0:
dictionary[node.path] = {}
for key in node._keys():
dictionary[node.path][key] = node._get(key)
for name in node._node_names():
self._add_node_to_dictionary(node._get_child(name), dictionary)
def _add_preferences_listener(self, listener):
""" Add a listener for changes to thisnode's preferences. """
with self._lk:
self._preferences_listeners.append(listener)
def _clear(self):
""" Remove all preferences from this node. """
with self._lk:
self._preferences.clear()
def _create_child(self, name):
""" Create a child of this node with the specified name. """
with self._lk:
child = self._children[name] = Preferences(name=name, parent=self)
return child
def _get(self, key, default=None):
""" Get the value of a preference in this node. """
with self._lk:
value = self._preferences.get(key, default)
return value
def _get_child(self, name):
"""Return the child of this node with the specified name.
Return None if no such child exists.
"""
with self._lk:
child = self._children.get(name)
return child
def _keys(self):
""" Return the preference keys of this node. """
with self._lk:
keys = list(self._preferences.keys())
return keys
def _node(self, name):
"""Return the child of this node with the specified name.
Create the child node if it does not exist.
"""
node = self._get_child(name)
if node is None:
node = self._create_child(name)
return node
def _node_names(self):
""" Return the names of the children of this node. """
with self._lk:
node_names = list(self._children.keys())
return node_names
def _remove(self, name):
""" Remove a preference value from this node. """
with self._lk:
if name in self._preferences:
del self._preferences[name]
def _remove_preferences_listener(self, listener):
""" Remove a listener for changes to the node's preferences. """
with self._lk:
if listener in self._preferences_listeners:
self._preferences_listeners.remove(listener)
def _set(self, key, value):
""" Set the value of a preference in this node. """
# everything must be unicode encoded so that ConfigObj configuration
# can properly serialize the data. Python str are supposed to be ASCII
# encoded.
value = str(value)
with self._lk:
old = self._preferences.get(key)
self._preferences[key] = value
# If the value is unchanged then don't call the listeners!
if old == value:
listeners = []
else:
listeners = self._preferences_listeners[:]
for listener in listeners:
listener(self, key, old, value)
###########################################################################
# Debugging interface.
###########################################################################
[docs] def dump(self, indent=""):
""" Dump the preferences hierarchy to stdout. """
if indent == "":
print()
print(indent, "Node(%s)" % self.name, self._preferences)
indent += " "
for child in self._children.values():
child.dump(indent)