From 332a5c2553db41de777473a1e1be9cd1522c9496 Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Thu, 31 Mar 2016 18:54:41 +0100 Subject: Move the src directory to poezio, for better cython compatibility. --- poezio/config.py | 685 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 685 insertions(+) create mode 100644 poezio/config.py (limited to 'poezio/config.py') 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 = '' -- cgit v1.2.3