Source code for src.core.configuration

"""Tools for keeping track of system settings.

The `configuration` module provides a system for managing the general, 
reusable settings for the software---both a collection of default settings and 
some user-specific settings---including default file locations, graph colors, 
graph update delays, and so on. It keeps track of these data in memory and saves
them to (and initially reads them from) a file on the disk.

The module defines some classes for the above-mentioned purposes, and it creates
an actual instance of the ``Configuration`` class, so that other modules can
access the configuration without having to create a new instance and, therefore
without having to re-read the file from the hard drive.
"""

from copy import deepcopy
import logging
import os
import subprocess
from tempfile import TemporaryFile

from src.tools import config_parser as cp
from src.tools import path_tools as pt

log = logging.getLogger('transport')

CONFIG_FOLDER = 'etc'
MAIN_PATH = pt.unrel([CONFIG_FOLDER, 'general.conf'])
EDITORS_PATH = pt.unrel([CONFIG_FOLDER, 'editors.conf'])
DEFAULT_FOLDER = os.path.expanduser('~')

[docs]class Configuration(object): """The `Configuration` class is a container for system-wide settings concerning appearance of certain components, default file locations, and helpful ways for letting users know that an experiment is finished or that serious system errors (e.g. magnet quenches) have occurred. """ def __init__(self): """Initialize a new Configuration object. Set default values for the various saved settings. Then attempt to read the data from the configuration file. If it works (i.e. the file exists and has the desired information), read the data from the file. If it does not, save the default data to the file. """ defaults = {('experiment_defaults', 'experiment_folder'): DEFAULT_FOLDER, ('file_defaults', 'data_folder'): DEFAULT_FOLDER, ('file_defaults', 'data_file'): 'data.dat', ('file_defaults', 'prepend_scan'): True, ('graph_defaults', 'graph_colors'): [(1, 0, 0)], ('graph_defaults', 'graph_delay'): 2.0, ('users', 'user_names'): [], ('miscellaneous_data', 'phone_carriers'): [('AT&T', 'txt.att.net'), ('Verizon', 'vtext.com')]} self._configParser = cp.ConfigParser(MAIN_PATH, cp.FORMAT_AUTO, defaultValues=defaults) self._experimentFolder = self._configParser.get('experiment_defaults', 'experiment_folder') self._dataFolder = self._configParser.get('file_defaults', 'data_folder') self._dataFile = self._configParser.get('file_defaults', 'data_file') self._prependScan = self._configParser.get('file_defaults', 'prepend_scan') self._graphColors = self._configParser.get('graph_defaults', 'graph_colors') self._graphDelay = self._configParser.get('graph_defaults', 'graph_delay') self._carriers = self._configParser.get('miscellaneous_data', 'phone_carriers') self._userNames = self._configParser.get('users', 'user_names') self._users = [] for userName in self._userNames: self._users.append(User(str(userName))) self._activeUser = None def _set(self, section, option, value): """Set the value associated with a specified key. This method is simply a shorthand for self._configParser.set Parameters ---------- section : str The section in which the key is located. option : str The key whose value is to be set. value : variable The new value to associate with the given key. Returns ------- variable The unchanged value of `value`. """ return self._configParser.set(section, option, value)
[docs] def getExperimentFolder(self, user=True): """Return the experiment folder. If a user is active and not overridden, return the user's default experiment folder. Otherwise, return the system default experiment folder. """ if not user or not self._activeUser: return self._experimentFolder return self._activeUser.getExperimentFolder()
[docs] def setExperimentFolder(self, newValue, user=True): """Set the default experiment file folder. If a user is active and not overridden, set the user's experiment file folder. Otherwise, set the system default experiment folder, which will be used whenever no user is active and will be the default for new users. """ if not user or not self._activeUser: self._experimentFolder = self._set('experiment_defaults', 'experiment_folder', newValue) else: self._activeUser.setExperimentFolder(newValue)
[docs] def getDataFolder(self, user=True): """Return the data folder. If a user is active and not overridden, return the user's default data folder. Otherwise, return the system default data folder. """ if not user or not self._activeUser: return self._dataFolder return self._activeUser.getDataFolder()
[docs] def setDataFolder(self, newValue, user=True): """Set the default data folder. If a user is active and not overridden, set the user's default data folder. Otherwise, set the system default data folder, which will be used whenever no user is active and will be the default for new users. """ if not user or not self._activeUser: self._dataFolder = self._set('file_defaults', 'data_folder', newValue) else: self._activeUser.setDataFolder(newValue)
[docs] def getDataFile(self, user=True): """Return the default filename. If a user is active and not overridden, return the user's default file name. Otherwise, return the system default data file name. """ if not user or not self._activeUser: return self._dataFile return self._activeUser.getDataFile()
[docs] def setDataFile(self, newValue, user=True): """Set the default filename. If a user is active and not overridden, set the user's default filename. Otherwise, set the system default filename, which will be used whenever no user is active and will be the default for new users. """ if not user or not self._activeUser: self._dataFile = self._set('file_defaults', 'data_file', newValue) else: self._activeUser.setDataFile(newValue)
[docs] def getPrependScan(self, user=True): """Return whether to prepend filenames with a scan number by default. If a user is active and not overridden, return whether the user wants to prepend a scan number by default. Otherwise, return whether the system default is to prepend a scan number. """ if not user or not self._activeUser: return self._prependScan return self._activeUser.getPrependScan()
[docs] def setPrependScan(self, newValue, user=True): """Set whether to prepend filenames with a scan number by default. If a user is active and not overridden, set whether the user wants to prepend a scan number by default. Otherwise, return whether the system default is to prepend a scan number. """ if not user or not self._activeUser: self._prependScan = self._set('file_defaults', 'prepend_scan', newValue) else: self._activeUser.setPrependScan(newValue)
[docs] def getGraphColors(self): """Return the list of graph colors (a list of RGB tuples).""" return deepcopy(self._graphColors)
[docs] def setGraphColors(self, newValue): """Set the colors (a list of RGB tuples) to use for plots.""" self._graphColors = self._set('graph_defaults', 'graph_colors', deepcopy(newValue))
[docs] def getGraphDelay(self): """Return the time between successive updates of plots.""" return self._graphDelay
[docs] def setGraphDelay(self, newValue): """Set the time between successive updates of plots.""" self._graphDelay = self._set('graph_defaults', 'graph_delay', deepcopy(newValue))
[docs] def getCarrierStrings(self): """Return a list of strings representing mobile carrier names.""" carrierStrings = [] for carrier in self._carriers: carrierStrings.append(carrier[0]) return carrierStrings
[docs] def loadUser(self, username): """Change the active user. Parameters ---------- username : str The username of the user whose data should be loaded. If 'None' or `None` is supplied, or if the specified user does not exist, the default settings will be loaded. """ if username in self._userNames: log.info('Loading user ' + str(username)) self._activeUser = self._users[self._userNames.index(username)] elif username == 'None' or username is None: log.info('Loading default user.') self._activeUser = None else: log.warn('User [%s] does not exist. Loading default.', username) self._activeUser = None
[docs] def getUserName(self): """Return the name of the active user. Returns ------- str The name of the active user, or 'None' if no user has been loaded. """ if self._activeUser is not None: return self._activeUser.getUserName() return 'None'
[docs] def setUserName(self, newValue): """Rename the active user. Parameters ---------- newValue : str The new value for the name of the current user, if one has been loaded. If no user is active, nothing will happen. """ if self._activeUser is not None: self._activeUser.setUserName(newValue)
[docs] def getPhone(self): """Return the phone number of the active user. Returns ------- str The phone number of the active user, or an empty string if there is no active user. """ if self._activeUser is not None: return self._activeUser.getPhone() return ''
[docs] def setPhone(self, newValue): """Set the phone number of the active user. Parameters ---------- newValue : str The new phone number for the active user. If there is no active user, nothing will happen. """ if self._activeUser is not None: self._activeUser.setPhone(newValue)
[docs] def getCarrier(self): """Return the carrier of the active user. Returns ------- str The carrier of the active user, or an empty string if there is no active user or no carrier associated with the active user. """ if self._activeUser is not None: return self._activeUser.getCarrier() return ''
[docs] def setCarrier(self, newValue): """Set the carrier of the active user, or do nothing if none.""" if self._activeUser is not None: self._activeUser.setCarrier(newValue)
[docs] def getSmsFinished(self): """Return whether to text the active user on experiment completion.""" if self._activeUser is not None: return self._activeUser.getSmsFinished() return False
[docs] def setSmsFinished(self, newValue): """Set whether to text the active user on experiment completion.""" if self._activeUser is not None: self._activeUser.setSmsFinished(newValue)
[docs] def getSmsError(self): """Return whether to text the active user on a system error.""" if self._activeUser is not None: return self._activeUser.getSmsError() return False
[docs] def setSmsError(self, newValue): """Set whether to text the active user on a system error.""" if self._activeUser is not None: self._activeUser.setSmsError(newValue)
[docs] def getUserNames(self): """Return a list of usernames as strings.""" names = [] for user in self._users: names.append(user.getUserName()) return names
[docs] def addUser(self, username): """Create a new user.""" log.info('Attempting to create user [%s].', username) self._users.append(User(username)) self._userNames.append(username) self._set('users', 'user_names', self._userNames) log.info('Adding user ' + username)
[docs] def removeUser(self, username): """Delete a user.""" log.info('Attempting to delete user [%s].', username) toDelete = -1 for index, user in enumerate(self._users): if user.getUserName() == username: if self._activeUser is user: self._activeUser = None toDelete = index if toDelete >= 0: log.info('Deleting user ' + username) del self._users[toDelete] del self._userNames[toDelete] self._set('users', 'user_names', self._userNames) userfile = pt.unrel(CONFIG_FOLDER, _getUserFile(username)) if os.path.exists(userfile): os.remove(userfile) else: log.warn('Cannot remove user [%s] because he does not exist.', username)
[docs]class User(object): """A class to assist Configuration by storing per-user settings""" defaults = {('experiment_defaults', 'experiment_folder'): DEFAULT_FOLDER, ('file_defaults', 'data_folder'): DEFAULT_FOLDER, ('file_defaults', 'data_file'): 'data.dat', ('file_defaults', 'prepend_scan'): True, ('personal_settings', 'phone'): '', ('personal_settings', 'carrier'): '', ('personal_settings', 'sms_finished'): False, ('personal_settings', 'sms_error'): False} def __init__(self, name): """Initialize a new user. Create a new user object with the given ``name``. If the proper file exists for the given user, the settings will be loaded from that. Otherwise, a new user will be created using the default settings, and the correct file will be created. """ userFile = pt.unrel(CONFIG_FOLDER, _getUserFile(name)) self._configParser = cp.ConfigParser(userFile, cp.FORMAT_AUTO, defaultValues=User.defaults) self._name = name self._experimentFolder = self._configParser.get('experiment_defaults', 'experiment_folder') self._dataFolder = self._configParser.get('file_defaults', 'data_folder') self._dataFile = self._configParser.get('file_defaults', 'data_file') self._prependScan = self._configParser.get('file_defaults', 'prepend_scan') self._phone = self._configParser.get('personal_settings', 'phone') self._carrier = self._configParser.get('personal_settings', 'carrier') self._smsFinished = self._configParser.get('personal_settings', 'sms_finished') self._smsError = self._configParser.get('personal_settings', 'sms_error') def _set(self, section, option, value): """Set the value associated with a specified key. This method is simply a shorthand for self._configParser.set Parameters ---------- section : str The section in which the key is located. option : str The key whose value is to be set. value : variable The new value to associate with the given key. Returns ------- variable The unchanged value of `value`. """ return self._configParser.set(section, option, value)
[docs] def getUserName(self): """Return the user's name.""" return self._name
[docs] def setUserName(self, newValue): """Set the user's name, changing the configuration file accordingly.""" oldFilename = pt.unrel(CONFIG_FOLDER, _getUserFile(self._name)) newFilename = pt.unrel(CONFIG_FOLDER, _getUserFile(newValue)) if oldFilename == newFilename: return log.info('Changing an existing username from [%s] to [%s].', self._name, newValue) with open(oldFilename, 'r') as oldFile: fileData = oldFile.read() os.remove(oldFilename) with open(newFilename, 'w') as newFile: newFile.write(fileData) self._configParser = cp.ConfigParser(newFilename, cp.FORMAT_AUTO, defaultValues=User.defaults) self._name = newValue
[docs] def getPhone(self): """Return the user's telephone number as a string.""" return self._phone
[docs] def setPhone(self, newValue): """Set the user's telephone number.""" self._phone = self._set('personal_settings', 'phone', newValue)
[docs] def getCarrier(self): """Return the user's phone carrier.""" return self._carrier
[docs] def setCarrier(self, newValue): """Set the user's phone carrier.""" self._carrier = self._set('personal_settings', 'carrier', newValue)
[docs] def getSmsFinished(self): """Return whether to text the user on experiment completion.""" return self._smsFinished
[docs] def setSmsFinished(self, newValue): """Set whether to text the user on experiment completion.""" self._smsFinished = self._set('personal_settings', 'sms_finished', newValue)
[docs] def getSmsError(self): """Return whether to text the user on critical system errors.""" return self._smsError
[docs] def setSmsError(self, newValue): """Set whether to text the user on critical system errors.""" self._smsError = self._set('personal_settings', 'sms_error', newValue)
[docs] def getExperimentFolder(self): """Return the user's default experiment file folder.""" return self._experimentFolder
[docs] def setExperimentFolder(self, newValue): """Set the user's default experiment file folder.""" self._experimentFolder = self._set('experiment_defaults', 'experiment_folder', newValue)
[docs] def getDataFolder(self): """Return the user's default data folder.""" return self._dataFolder
[docs] def setDataFolder(self, newValue): """Set the user's default data folder.""" self._dataFolder = self._set('file_defaults', 'data_folder', newValue)
[docs] def getDataFile(self): """Return the default filename for the user's data.""" return self._dataFile
[docs] def setDataFile(self, newValue): """Set the default filename for the user's data.""" self._dataFile = self._set('file_defaults', 'data_file', newValue)
[docs] def getPrependScan(self): """Return whether the user wants to prepend scan numbers by default.""" return self._prependScan
[docs] def setPrependScan(self, newValue): """Set whether the user wants to prepend scan numbers by default.""" self._prependScan = self._set('file_defaults', 'prepend_scan', newValue)
def _getUserFile(username): """Get the appropriate filename for a specified user name. Parameters ---------- username : str The name of the user whose configuration filename should be returned. Returns ------- str The filename for the appropriate user's configuration file. """ return 'user_' + str(username) + '.conf'
[docs]def which(program): """Return the path to a specified program, or None if the path doesn't work. Parameters ---------- program : str The path for a program. Returns ------- The input path `program` if it refers to a file which exists and can be accessed by the Python interpreter. Otherwise, `None`. Notes ----- If `program` does not refer to an absolute path, it will be appended to the elements in the system path, and if any of those work, the absolute path formed thereby will be returned. """ def isExecutable(fpath): """Return whether the path exists and can be accessed.""" return os.path.isfile(fpath) and os.access(fpath, os.X_OK) fpath = os.path.split(program)[0] if fpath: if isExecutable(program): return program else: for path in os.environ["PATH"].split(os.pathsep): path = path.strip('"') exeFile = os.path.join(path, program) if isExecutable(exeFile): return exeFile return None
[docs]def getEditors(): """Return a list of text file editors. Returns ------- list of str A list of text editors which exist in the filesystem and can be accessed by this software. """ editorConfig = cp.ConfigParser(EDITORS_PATH, cp.FORMAT_AUTO) goodEditors = [] for key in editorConfig.getOptions('editors'): editor = editorConfig.get('editors', key) if isinstance(editor, list): editor = pt.unrel(editor) if which(editor) is not None: goodEditors.append(editor) log.info('Good editors: ' + str(goodEditors)) return goodEditors
[docs]def openEditor(defaultText=''): """Open a temporary file in a text editor.""" with TemporaryFile() as tempFile: name = tempFile.name + '.py' with open(name, 'w') as tempFile: tempFile.write(defaultText) editors = getEditors() for editor in editors: try: subprocess.call([editor, name], shell=False) break except OSError: continue data = open(name).read() os.remove(name) return data
c = Configuration()