#!/usr/bin/env python
# -*- coding: utf-8 -*-
import errno
import os
import sys
import platform
from pathlib import Path
from .. import __version__
from pkg_resources import parse_version
import shutil
try:
import configobj
if (sys.version_info.minor >= 7 and
parse_version(configobj.__version__) < parse_version('5.1.0')):
raise ImportError('Installed configobj does not support Python 3.7+')
_haveConfigobj = True
except ImportError:
_haveConfigobj = False
if _haveConfigobj: # Use the "global" installation.
from configobj import ConfigObj
try:
from configobj import validate
except ImportError: # Older versions of configobj
import validate
else: # Use our contrib package if configobj is not installed or too old.
from psychopy.contrib import configobj
from psychopy.contrib.configobj import ConfigObj
from psychopy.contrib.configobj import validate
join = os.path.join
[docs]class Preferences:
"""Users can alter preferences from the dialog box in the application,
by editing their user preferences file (which is what the dialog box does)
or, within a script, preferences can be controlled like this::
import psychopy
psychopy.prefs.hardware['audioLib'] = ['ptb', 'pyo','pygame']
print(psychopy.prefs)
# prints the location of the user prefs file and all the current vals
Use the instance of `prefs`, as above, rather than the `Preferences` class
directly if you want to affect the script that's running.
"""
# Names of legacy parameters which are needed for use version
legacy = [
"winType", # 2023.1.0
"audioLib", # 2023.1.0
"audioLatencyMode", # 2023.1.0
]
def __init__(self):
super(Preferences, self).__init__()
self.userPrefsCfg = None # the config object for the preferences
self.prefsSpec = None # specifications for the above
# the config object for the app data (users don't need to see)
self.appDataCfg = None
self.general = None
self.piloting = None
self.coder = None
self.builder = None
self.connections = None
self.paths = {} # this will remain a dictionary
self.keys = {} # does not remain a dictionary
self.getPaths()
self.loadAll()
# setting locale is now handled in psychopy.localization.init
# as called upon import by the app
if self.userPrefsCfg['app']['resetPrefs']:
self.resetPrefs()
def __str__(self):
"""pretty printing the current preferences"""
strOut = "psychopy.prefs <%s>:\n" % (
join(self.paths['userPrefsDir'], 'userPrefs.cfg'))
for sectionName in ['general', 'coder', 'builder', 'connections']:
section = getattr(self, sectionName)
for key, val in list(section.items()):
strOut += " prefs.%s['%s'] = %s\n" % (
sectionName, key, repr(val))
return strOut
[docs] def resetPrefs(self):
"""removes userPrefs.cfg, does not touch appData.cfg
"""
userCfg = join(self.paths['userPrefsDir'], 'userPrefs.cfg')
try:
os.unlink(userCfg)
except Exception:
msg = "Could not remove prefs file '%s'; (try doing it manually?)"
print(msg % userCfg)
self.loadAll() # reloads, now getting all from .spec
def getPaths(self):
# on mac __file__ might be a local path, so make it the full path
thisFileAbsPath = os.path.abspath(__file__)
prefSpecDir = os.path.split(thisFileAbsPath)[0]
dirPsychoPy = os.path.split(prefSpecDir)[0]
exePath = sys.executable
# path to Resources (icons etc)
dirApp = join(dirPsychoPy, 'app')
if os.path.isdir(join(dirApp, 'Resources')):
dirResources = join(dirApp, 'Resources')
else:
dirResources = dirApp
self.paths['psychopy'] = dirPsychoPy
self.paths['appDir'] = dirApp
self.paths['appFile'] = join(dirApp, 'PsychoPy.py')
self.paths['demos'] = join(dirPsychoPy, 'demos')
self.paths['resources'] = dirResources
self.paths['tests'] = join(dirPsychoPy, 'tests')
# path to libs/frameworks
if 'PsychoPy.app/Contents' in exePath:
self.paths['libs'] = exePath.replace("MacOS/python", "Frameworks")
else:
self.paths['libs'] = '' # we don't know where else to look!
if not Path(self.paths['appDir']).is_dir():
# if there isn't an app folder at all then this is a lib-only psychopy
# so don't try to load app prefs etc
NO_APP = True
if sys.platform == 'win32':
self.paths['prefsSpecFile'] = join(prefSpecDir, 'Windows.spec')
self.paths['userPrefsDir'] = join(os.environ['APPDATA'],
'psychopy3')
else:
self.paths['prefsSpecFile'] = join(prefSpecDir,
platform.system() + '.spec')
self.paths['userPrefsDir'] = join(os.environ['HOME'],
'.psychopy3')
# directory for files created by the app at runtime needed for operation
self.paths['userCacheDir'] = join(self.paths['userPrefsDir'], 'cache')
# paths in user directory to create/check write access
userPrefsPaths = (
'userPrefsDir', # root dir
'themes', # define theme path
'fonts', # find / copy fonts
'packages', # packages and plugins
'cache', # cache for downloaded and other temporary files
)
# build directory structure inside user directory
for userPrefPath in userPrefsPaths:
# define path
if userPrefPath != 'userPrefsDir': # skip creating root, just check
self.paths[userPrefPath] = join(
self.paths['userPrefsDir'],
userPrefPath)
# avoid silent fail-to-launch-app if bad permissions:
try:
os.makedirs(self.paths[userPrefPath])
except OSError as err:
if err.errno != errno.EEXIST:
raise
# root site-packages directory for user-installed packages and add it
pyVer = 'Python{}{}'.format(
sys.version_info.major, sys.version_info.minor)
prefixRootDir = Path(
self.paths['userPrefsDir']) / 'packages' / pyVer
# populate directory structure for user-installed packages
if not prefixRootDir.is_dir():
prefixRootDir.mkdir()
userSiteDir = prefixRootDir / 'site-packages'
if not userSiteDir.is_dir():
userSiteDir.mkdir()
# Scripts directory for user-installed packages
userScriptsDir = prefixRootDir / 'Scripts'
if not userScriptsDir.is_dir():
userScriptsDir.mkdir()
# add paths from plugins/packages (installed by plugins manager)
self.paths['userPackages'] = userSiteDir
self.paths['userScripts'] = userScriptsDir
# Get dir for base and user themes
baseThemeDir = Path(self.paths['appDir']) / "themes" / "spec"
userThemeDir = Path(self.paths['themes'])
# Check what version user themes were last updated in
if (userThemeDir / "last.ver").is_file():
with open(userThemeDir / "last.ver", "r") as f:
lastVer = parse_version(f.read())
else:
# if no version available, assume it was the first version to have themes
lastVer = parse_version("2020.2.0")
# If version has changed since base themes last copied, they need updating
updateThemes = lastVer < parse_version(__version__)
# Copy base themes to user themes folder if missing or need update
for file in baseThemeDir.glob("*.json"):
if updateThemes or not (Path(self.paths['themes']) / file.name).is_file():
shutil.copyfile(
file,
Path(self.paths['themes']) / file.name
)
[docs] def loadAll(self):
"""Load the user prefs and the application data
"""
self._validator = validate.Validator()
# note: self.paths['userPrefsDir'] gets set in loadSitePrefs()
self.paths['appDataFile'] = join(
self.paths['userPrefsDir'], 'appData.cfg')
self.paths['userPrefsFile'] = join(
self.paths['userPrefsDir'], 'userPrefs.cfg')
# If PsychoPy is tucked away by Py2exe in library.zip, the preferences
# file cannot be found. This hack is an attempt to fix this.
libzip = "\\library.zip\\psychopy\\preferences\\"
if libzip in self.paths["prefsSpecFile"]:
self.paths["prefsSpecFile"] = self.paths["prefsSpecFile"].replace(
libzip, "\\resources\\")
self.userPrefsCfg = self.loadUserPrefs()
self.appDataCfg = self.loadAppData()
self.validate()
# simplify namespace
self.general = self.userPrefsCfg['general']
self.app = self.userPrefsCfg['app']
self.coder = self.userPrefsCfg['coder']
self.builder = self.userPrefsCfg['builder']
self.hardware = self.userPrefsCfg['hardware']
self.piloting = self.userPrefsCfg['piloting']
self.connections = self.userPrefsCfg['connections']
self.appData = self.appDataCfg
# keybindings:
self.keys = self.userPrefsCfg['keyBindings']
[docs] def loadUserPrefs(self):
"""load user prefs, if any; don't save to a file because doing so
will break easy_install. Saving to files within the psychopy/ is
fine, eg for key-bindings, but outside it (where user prefs will
live) is not allowed by easy_install (security risk)
"""
self.prefsSpec = ConfigObj(self.paths['prefsSpecFile'],
encoding='UTF8', list_values=False)
# check/create path for user prefs
if not os.path.isdir(self.paths['userPrefsDir']):
try:
os.makedirs(self.paths['userPrefsDir'])
except Exception:
msg = ("Preferences.py failed to create folder %s. Settings"
" will be read-only")
print(msg % self.paths['userPrefsDir'])
# then get the configuration file
cfg = ConfigObj(self.paths['userPrefsFile'],
encoding='UTF8', configspec=self.prefsSpec)
# cfg.validate(self._validator, copy=False) # merge then validate
# don't cfg.write(), see explanation above
return cfg
[docs] def saveUserPrefs(self):
"""Validate and save the various setting to the appropriate files
(or discard, in some cases)
"""
self.validate()
if not os.path.isdir(self.paths['userPrefsDir']):
os.makedirs(self.paths['userPrefsDir'])
self.userPrefsCfg.write()
[docs] def loadAppData(self):
"""Fetch app data config (unless this is a lib-only installation)
"""
appDir = Path(self.paths['appDir'])
if not appDir.is_dir(): # if no app dir this may be just lib install
return {}
# fetch appData too against a config spec
appDataSpec = ConfigObj(join(self.paths['appDir'], 'appData.spec'),
encoding='UTF8', list_values=False)
cfg = ConfigObj(self.paths['appDataFile'],
encoding='UTF8', configspec=appDataSpec)
resultOfValidate = cfg.validate(self._validator,
copy=True,
preserve_errors=True)
self.restoreBadPrefs(cfg, resultOfValidate)
# force favComponent level values to be integers
if 'favComponents' in cfg['builder']:
for key in cfg['builder']['favComponents']:
_compKey = cfg['builder']['favComponents'][key]
cfg['builder']['favComponents'][key] = int(_compKey)
return cfg
[docs] def saveAppData(self):
"""Save the various setting to the appropriate files
(or discard, in some cases)
"""
# copy means all settings get saved:
self.appDataCfg.validate(self._validator, copy=True)
if not os.path.isdir(self.paths['userPrefsDir']):
os.makedirs(self.paths['userPrefsDir'])
self.appDataCfg.write()
[docs] def validate(self):
"""Validate (user) preferences and reset invalid settings to defaults
"""
result = self.userPrefsCfg.validate(self._validator, copy=True)
self.restoreBadPrefs(self.userPrefsCfg, result)
[docs] def restoreBadPrefs(self, cfg, result):
"""result = result of validate
"""
if result == True:
return
vtor = validate.Validator()
for sectionList, key, _ in configobj.flatten_errors(cfg, result):
if key is not None:
_secList = ', '.join(sectionList)
val = cfg.configspec[_secList][key]
cfg[_secList][key] = vtor.get_default_value(val)
else:
msg = "Section [%s] was missing in file '%s'"
print(msg % (', '.join(sectionList), cfg.filename))
prefs = Preferences()