summaryrefslogtreecommitdiff
path: root/poezio/config.py
diff options
context:
space:
mode:
authorEmmanuel Gil Peyrot <linkmauve@linkmauve.fr>2016-03-31 18:54:41 +0100
committerEmmanuel Gil Peyrot <linkmauve@linkmauve.fr>2016-06-11 20:49:43 +0100
commit332a5c2553db41de777473a1e1be9cd1522c9496 (patch)
tree3ee06a59f147ccc4009b35cccfbe2461bcd18310 /poezio/config.py
parentcf44cf7cdec9fdb35caa372563d57e7045dc29dd (diff)
downloadpoezio-332a5c2553db41de777473a1e1be9cd1522c9496.tar.gz
poezio-332a5c2553db41de777473a1e1be9cd1522c9496.tar.bz2
poezio-332a5c2553db41de777473a1e1be9cd1522c9496.tar.xz
poezio-332a5c2553db41de777473a1e1be9cd1522c9496.zip
Move the src directory to poezio, for better cython compatibility.
Diffstat (limited to 'poezio/config.py')
-rw-r--r--poezio/config.py685
1 files changed, 685 insertions, 0 deletions
diff --git a/poezio/config.py b/poezio/config.py
new file mode 100644
index 00000000..7f0c75f6
--- /dev/null
+++ b/poezio/config.py
@@ -0,0 +1,685 @@
+"""
+Defines the global config instance, used to get or set (and save) values
+from/to the config file.
+
+This module has the particularity that some imports and global variables
+are delayed because it would mean doing an incomplete setup of the python
+loggers.
+
+TODO: get http://bugs.python.org/issue1410680 fixed, one day, in order
+to remove our ugly custom I/O methods.
+"""
+
+DEFSECTION = "Poezio"
+
+import logging.config
+import os
+import stat
+import sys
+import pkg_resources
+
+from configparser import RawConfigParser, NoOptionError, NoSectionError
+from os import environ, makedirs, path, remove
+from shutil import copy2
+from args import parse_args
+
+DEFAULT_CONFIG = {
+ 'Poezio': {
+ 'ack_message_receipts': True,
+ 'add_space_after_completion': True,
+ 'after_completion': ',',
+ 'alternative_nickname': '',
+ 'auto_reconnect': True,
+ 'autorejoin_delay': '5',
+ 'autorejoin': False,
+ 'beep_on': 'highlight private invite',
+ 'ca_cert_path': '',
+ 'certificate': '',
+ 'certfile': '',
+ 'ciphers': 'HIGH+kEDH:HIGH+kEECDH:HIGH:!PSK:!SRP:!3DES:!aNULL',
+ 'connection_check_interval': 60,
+ 'connection_timeout_delay': 10,
+ 'create_gaps': False,
+ 'custom_host': '',
+ 'custom_port': '',
+ 'default_nick': '',
+ 'deterministic_nick_colors': True,
+ 'nick_color_aliases': True,
+ 'display_activity_notifications': False,
+ 'display_gaming_notifications': False,
+ 'display_mood_notifications': False,
+ 'display_tune_notifications': False,
+ 'display_user_color_in_join_part': True,
+ 'enable_carbons': True,
+ 'enable_user_activity': True,
+ 'enable_user_gaming': True,
+ 'enable_user_mood': True,
+ 'enable_user_nick': True,
+ 'enable_user_tune': True,
+ 'enable_vertical_tab_list': False,
+ 'enable_xhtml_im': True,
+ 'eval_password': '',
+ 'exec_remote': False,
+ 'extract_inline_images': True,
+ 'filter_info_messages': '',
+ 'force_encryption': True,
+ 'force_remote_bookmarks': False,
+ 'go_to_previous_tab_on_alt_number': False,
+ 'group_corrections': True,
+ 'hide_exit_join': -1,
+ 'hide_status_change': 120,
+ 'hide_user_list': False,
+ 'highlight_on': '',
+ 'ignore_certificate': False,
+ 'ignore_private': False,
+ 'information_buffer_popup_on': 'error roster warning help info',
+ 'jid': '',
+ 'keyfile': '',
+ 'lang': 'en',
+ 'lazy_resize': True,
+ 'load_log': 10,
+ 'log_dir': '',
+ 'log_errors': True,
+ 'max_lines_in_memory': 2048,
+ 'max_messages_in_memory': 2048,
+ 'max_nick_length': 25,
+ 'muc_history_length': 50,
+ 'notify_messages': True,
+ 'open_all_bookmarks': False,
+ 'password': '',
+ 'plugins_autoload': '',
+ 'plugins_conf_dir': '',
+ 'plugins_dir': '',
+ 'popup_time': 4,
+ 'private_auto_response': '',
+ 'remote_fifo_path': './',
+ 'request_message_receipts': True,
+ 'resource': '',
+ 'rooms': '',
+ 'roster_group_sort': 'name',
+ 'roster_show_offline': False,
+ 'roster_sort': 'jid:show',
+ 'save_status': True,
+ 'self_ping_delay': 0,
+ 'send_chat_states': True,
+ 'send_initial_presence': True,
+ 'send_os_info': True,
+ 'send_poezio_info': True,
+ 'send_time': True,
+ 'separate_history': False,
+ 'server': 'anon.jeproteste.info',
+ 'show_composing_tabs': 'direct',
+ 'show_inactive_tabs': True,
+ 'show_jid_in_conversations': True,
+ 'show_muc_jid': True,
+ 'show_roster_jids': True,
+ 'show_roster_subscriptions': '',
+ 'show_s2s_errors': True,
+ 'show_tab_names': False,
+ 'show_tab_numbers': True,
+ 'show_timestamps': True,
+ 'show_useless_separator': True,
+ 'status': '',
+ 'status_message': '',
+ 'theme': 'default',
+ 'themes_dir': '',
+ 'tmp_image_dir': '',
+ 'use_bookmarks_method': '',
+ 'use_log': False,
+ 'use_remote_bookmarks': True,
+ 'user_list_sort': 'desc',
+ 'use_tab_nicks': True,
+ 'vertical_tab_list_size': 20,
+ 'vertical_tab_list_sort': 'desc',
+ 'whitespace_interval': 300,
+ 'words': ''
+ },
+ 'bindings': {
+ 'M-i': '^I'
+ },
+ 'var': {
+ 'folded_roster_groups': '',
+ 'info_win_height': 2
+ },
+ 'muc_colors': {
+ }
+}
+
+class Config(RawConfigParser):
+ """
+ load/save the config to a file
+ """
+ def __init__(self, file_name, default=None):
+ RawConfigParser.__init__(self, None)
+ # make the options case sensitive
+ self.optionxform = str
+ self.file_name = file_name
+ self.read_file()
+ self.default = default
+
+ def read_file(self):
+ try:
+ RawConfigParser.read(self, self.file_name, encoding='utf-8')
+ except TypeError: # python < 3.2 sucks
+ RawConfigParser.read(self, self.file_name)
+ # Check config integrity and fix it if it’s wrong
+ # only when the object is the main config
+ if self.__class__ is Config:
+ for section in ('bindings', 'var'):
+ if not self.has_section(section):
+ self.add_section(section)
+
+ def get(self, option, default=None, section=DEFSECTION):
+ """
+ get a value from the config but return
+ a default value if it is not found
+ The type of default defines the type
+ returned
+ """
+ if default is None:
+ if self.default:
+ default = self.default.get(section, {}).get(option)
+ else:
+ default = ''
+
+ try:
+ if type(default) == int:
+ res = self.getint(option, section)
+ elif type(default) == float:
+ res = self.getfloat(option, section)
+ elif type(default) == bool:
+ res = self.getboolean(option, section)
+ else:
+ res = self.getstr(option, section)
+ except (NoOptionError, NoSectionError, ValueError, AttributeError):
+ return default
+
+ if res is None:
+ return default
+ return res
+
+ def get_by_tabname(self, option, tabname,
+ fallback=True, fallback_server=True, default=''):
+ """
+ Try to get the value for the option. First we look in
+ a section named `tabname`, if the option is not present
+ in the section, we search for the global option if fallback is
+ True. And we return `default` as a fallback as a last resort.
+ """
+ if self.default and (not default) and fallback:
+ default = self.default.get(DEFSECTION, {}).get(option, '')
+ if tabname in self.sections():
+ if option in self.options(tabname):
+ # We go the tab-specific option
+ return self.get(option, default, tabname)
+ if fallback_server:
+ return self.get_by_servname(tabname, option, default, fallback)
+ if fallback:
+ # We fallback to the global option
+ return self.get(option, default)
+ return default
+
+ def get_by_servname(self, jid, option, default, fallback=True):
+ """
+ Try to get the value of an option for a server
+ """
+ server = safeJID(jid).server
+ if server:
+ server = '@' + server
+ if server in self.sections() and option in self.options(server):
+ return self.get(option, default, server)
+ if fallback:
+ return self.get(option, default)
+ return default
+
+
+ def __get(self, option, section=DEFSECTION, **kwargs):
+ """
+ facility for RawConfigParser.get
+ """
+ return RawConfigParser.get(self, section, option, **kwargs)
+
+ def _get(self, section, conv, option, **kwargs):
+ """
+ Redirects RawConfigParser._get
+ """
+ return conv(self.__get(option, section, **kwargs))
+
+ def getstr(self, option, section=DEFSECTION):
+ """
+ get a value and returns it as a string
+ """
+ return self.__get(option, section)
+
+ def getint(self, option, section=DEFSECTION):
+ """
+ get a value and returns it as an int
+ """
+ return RawConfigParser.getint(self, section, option)
+
+ def getfloat(self, option, section=DEFSECTION):
+ """
+ get a value and returns it as a float
+ """
+ return RawConfigParser.getfloat(self, section, option)
+
+ def getboolean(self, option, section=DEFSECTION):
+ """
+ get a value and returns it as a boolean
+ """
+ return RawConfigParser.getboolean(self, section, option)
+
+ def write_in_file(self, section, option, value):
+ """
+ Our own way to save write the value in the file
+ Just find the right section, and then find the
+ right option, and edit it.
+ """
+ result = self._parse_file()
+ if not result:
+ return False
+ else:
+ sections, result_lines = result
+
+ if not section in sections:
+ result_lines.append('[%s]' % section)
+ result_lines.append('%s = %s' % (option, value))
+ else:
+ begin, end = sections[section]
+ pos = find_line(result_lines, begin, end, option)
+
+ if pos is -1:
+ result_lines.insert(end, '%s = %s' % (option, value))
+ else:
+ result_lines[pos] = '%s = %s' % (option, value)
+
+ return self._write_file(result_lines)
+
+ def remove_in_file(self, section, option):
+ """
+ Our own way to remove an option from the file.
+ """
+ result = self._parse_file()
+ if not result:
+ return False
+ else:
+ sections, result_lines = result
+
+ if not section in sections:
+ log.error('Tried to remove the option %s from a non-'
+ 'existing section (%s)', option, section)
+ return True
+ else:
+ begin, end = sections[section]
+ pos = find_line(result_lines, begin, end, option)
+
+ if pos is -1:
+ log.error('Tried to remove a non-existing option %s'
+ ' from section %s', option, section)
+ return True
+ else:
+ del result_lines[pos]
+
+ return self._write_file(result_lines)
+
+ def _write_file(self, lines):
+ """
+ Write the config file, write to a temporary file
+ before copying it to the final destination
+ """
+ try:
+ prefix, file = path.split(self.file_name)
+ filename = path.join(prefix, '.%s.tmp' % file)
+ fd = os.fdopen(
+ os.open(
+ filename,
+ os.O_WRONLY | os.O_CREAT,
+ 0o600),
+ 'w')
+ for line in lines:
+ fd.write('%s\n' % line)
+ fd.close()
+ copy2(filename, self.file_name)
+ remove(filename)
+ except:
+ success = False
+ log.error('Unable to save the config file.', exc_info=True)
+ else:
+ success = True
+ return success
+
+ def _parse_file(self):
+ """
+ Parse the config file and return the list of sections with
+ their start and end positions, and the lines in the file.
+
+ Duplicate sections are preserved but ignored for the parsing.
+
+ Returns an empty tuple if reading fails
+ """
+ if file_ok(self.file_name):
+ try:
+ with open(self.file_name, 'r', encoding='utf-8') as df:
+ lines_before = [line.strip() for line in df]
+ except:
+ log.error('Unable to read the config file %s',
+ self.file_name,
+ exc_info=True)
+ return tuple()
+ else:
+ lines_before = []
+
+ sections = {}
+ duplicate_section = False
+ current_section = ''
+ current_line = 0
+
+ for line in lines_before:
+ if line.startswith('['):
+ if not duplicate_section and current_section:
+ sections[current_section][1] = current_line
+
+ duplicate_section = False
+ current_section = line[1:-1]
+
+ if current_section in sections:
+ log.error('Error while reading the configuration file,'
+ ' skipping until next section')
+ duplicate_section = True
+ else:
+ sections[current_section] = [current_line, current_line]
+
+ current_line += 1
+ if not duplicate_section and current_section:
+ sections[current_section][1] = current_line
+
+ return (sections, lines_before)
+
+ def set_and_save(self, option, value, section=DEFSECTION):
+ """
+ set the value in the configuration then save it
+ to the file
+ """
+ # Special case for a 'toggle' value. We take the current value
+ # and set the opposite. Warning if the no current value exists
+ # or it is not a bool.
+ if value == "toggle":
+ current = self.get(option, "", section)
+ if isinstance(current, bool):
+ value = str(not current)
+ else:
+ if current.lower() == "false":
+ value = "true"
+ elif current.lower() == "true":
+ value = "false"
+ else:
+ return ('Could not toggle option: %s.'
+ ' Current value is %s.' %
+ (option, current or "empty"),
+ 'Warning')
+ if self.has_section(section):
+ RawConfigParser.set(self, section, option, value)
+ else:
+ self.add_section(section)
+ RawConfigParser.set(self, section, option, value)
+ if not self.write_in_file(section, option, value):
+ return ('Unable to write in the config file', 'Error')
+ return ("%s=%s" % (option, value), 'Info')
+
+ def remove_and_save(self, option, section=DEFSECTION):
+ """
+ Remove an option and then save it the config file
+ """
+ if self.has_section(section):
+ RawConfigParser.remove_option(self, section, option)
+ if not self.remove_in_file(section, option):
+ return ('Unable to save the config file', 'Error')
+ return ('Option %s deleted' % option, 'Info')
+
+ def silent_set(self, option, value, section=DEFSECTION):
+ """
+ Set a value, save, and return True on success and False on failure
+ """
+ if self.has_section(section):
+ RawConfigParser.set(self, section, option, value)
+ else:
+ self.add_section(section)
+ RawConfigParser.set(self, section, option, value)
+ return self.write_in_file(section, option, value)
+
+ def set(self, option, value, section=DEFSECTION):
+ """
+ Set the value of an option temporarily
+ """
+ try:
+ RawConfigParser.set(self, section, option, value)
+ except NoSectionError:
+ pass
+
+ def to_dict(self):
+ """
+ Returns a dict of the form {section: {option: value, option: value}, …}
+ """
+ res = {}
+ for section in self.sections():
+ res[section] = {}
+ for option in self.options(section):
+ res[section][option] = self.get(option, "", section)
+ return res
+
+
+def find_line(lines, start, end, option):
+ """
+ Get the number of the line containing the option in the
+ relevant part of the config file.
+
+ Returns -1 if the option isn’t found
+ """
+ current = start
+ for line in lines[start:end]:
+ if (line.startswith('%s ' % option) or
+ line.startswith('%s=' % option)):
+ return current
+ current += 1
+ return -1
+
+def file_ok(filepath):
+ """
+ Returns True if the file exists and is readable and writeable,
+ False otherwise.
+ """
+ val = path.exists(filepath)
+ val &= os.access(filepath, os.R_OK | os.W_OK)
+ return bool(val)
+
+def check_create_config_dir():
+ """
+ create the configuration directory if it doesn't exist
+ """
+ CONFIG_HOME = environ.get("XDG_CONFIG_HOME")
+ if not CONFIG_HOME:
+ CONFIG_HOME = path.join(environ.get('HOME'), '.config')
+ CONFIG_PATH = path.join(CONFIG_HOME, 'poezio')
+
+ try:
+ makedirs(CONFIG_PATH)
+ except OSError:
+ pass
+ return CONFIG_PATH
+
+def check_create_cache_dir():
+ """
+ create the cache directory if it doesn't exist
+ also create the subdirectories
+ """
+ global CACHE_DIR
+ CACHE_HOME = environ.get("XDG_CACHE_HOME")
+ if not CACHE_HOME:
+ CACHE_HOME = path.join(environ.get('HOME'), '.cache')
+ CACHE_DIR = path.join(CACHE_HOME, 'poezio')
+
+ try:
+ makedirs(CACHE_DIR)
+ makedirs(path.join(CACHE_DIR, 'images'))
+ except OSError:
+ pass
+
+def check_config():
+ """
+ Check the config file and print results
+ """
+ result = {'missing': [], 'changed': []}
+ for option in DEFAULT_CONFIG['Poezio']:
+ value = config.get(option)
+ if value != DEFAULT_CONFIG['Poezio'][option]:
+ result['changed'].append((option, value, DEFAULT_CONFIG['Poezio'][option]))
+ else:
+ value = config.get(option, default='')
+ upper = value.upper()
+ default = str(DEFAULT_CONFIG['Poezio'][option]).upper()
+ if upper != default:
+ result['missing'].append(option)
+
+ result['changed'].sort(key=lambda x: x[0])
+ result['missing'].sort()
+ if result['changed']:
+ print('\033[1mOptions changed from the default configuration:\033[0m\n')
+ for option, new_value, default in result['changed']:
+ print(' \033[1m%s\033[0m = \033[33m%s\033[0m (default: \033[32m%s\033[0m)' % (option, new_value, default))
+
+ if result['missing']:
+ print('\n\033[1mMissing options:\033[0m (the defaults are used)\n')
+ for option in result['missing']:
+ print(' \033[31m%s\033[0m' % option)
+
+def run_cmdline_args(CONFIG_PATH):
+ "Parse the command line arguments"
+ global options
+ options = parse_args(CONFIG_PATH)
+
+ # Copy a default file if none exists
+ if not path.isfile(options.filename):
+ default = path.join(path.dirname(__file__), '../data/default_config.cfg')
+ other = pkg_resources.resource_filename('poezio', 'default_config.cfg')
+ if path.isfile(default):
+ copy2(default, options.filename)
+ elif path.isfile(other):
+ copy2(other, options.filename)
+
+ # Inside the nixstore and possibly other distributions, the reference
+ # file is readonly, so is the copy.
+ # Make it writable by the user who just created it.
+ if os.path.exists(options.filename):
+ os.chmod(options.filename,
+ os.stat(options.filename).st_mode | stat.S_IWUSR)
+
+ global firstrun
+ firstrun = True
+
+def create_global_config():
+ "Create the global config object, or crash"
+ try:
+ global config
+ config = Config(options.filename, DEFAULT_CONFIG)
+ except:
+ import traceback
+ sys.stderr.write('Poezio was unable to read or'
+ ' parse the config file.\n')
+ traceback.print_exc(limit=0)
+ sys.exit(1)
+
+def check_create_log_dir():
+ "Create the poezio logging directory if it doesn’t exist"
+ global LOG_DIR
+ LOG_DIR = config.get('log_dir')
+
+ if not LOG_DIR:
+
+ data_dir = environ.get('XDG_DATA_HOME')
+ if not data_dir:
+ home = environ.get('HOME')
+ data_dir = path.join(home, '.local', 'share')
+
+ LOG_DIR = path.join(data_dir, 'poezio', 'logs')
+
+ LOG_DIR = path.expanduser(LOG_DIR)
+
+ try:
+ makedirs(LOG_DIR)
+ except:
+ pass
+
+def setup_logging():
+ "Change the logging config according to the cmdline options and config"
+ if config.get('log_errors'):
+ LOGGING_CONFIG['root']['handlers'].append('error')
+ LOGGING_CONFIG['handlers']['error'] = {
+ 'level': 'ERROR',
+ 'class': 'logging.FileHandler',
+ 'filename': path.join(LOG_DIR, 'errors.log'),
+ 'formatter': 'simple',
+ }
+
+ if options.debug:
+ LOGGING_CONFIG['root']['handlers'].append('debug')
+ LOGGING_CONFIG['handlers']['debug'] = {
+ 'level':'DEBUG',
+ 'class':'logging.FileHandler',
+ 'filename': options.debug,
+ 'formatter': 'simple',
+ }
+
+
+ if LOGGING_CONFIG['root']['handlers']:
+ logging.config.dictConfig(LOGGING_CONFIG)
+ else:
+ logging.basicConfig(level=logging.CRITICAL)
+
+ global log
+ log = logging.getLogger(__name__)
+
+def post_logging_setup():
+ # common imports slixmpp, which creates then its loggers, so
+ # it needs to be after logger configuration
+ from common import safeJID as JID
+ global safeJID
+ safeJID = JID
+
+LOGGING_CONFIG = {
+ 'version': 1,
+ 'disable_existing_loggers': True,
+ 'formatters': {
+ 'simple': {
+ 'format': '%(asctime)s %(levelname)s:%(module)s:%(message)s'
+ }
+ },
+ 'handlers': {
+ },
+ 'root': {
+ 'handlers': [],
+ 'propagate': True,
+ 'level': 'DEBUG',
+ }
+}
+
+# True if this is the first run, in this case we will display
+# some help in the info buffer
+firstrun = False
+
+# Global config object. Is setup in poezio.py
+config = None
+
+# The logger object for this module
+log = None
+
+# The command-line options
+options = None
+
+# delayed import from common.py
+safeJID = None
+
+# the global log dir
+LOG_DIR = ''
+
+# the global cache dir
+CACHE_DIR = ''