diff options
Diffstat (limited to 'poezio/config.py')
-rw-r--r-- | poezio/config.py | 289 |
1 files changed, 155 insertions, 134 deletions
diff --git a/poezio/config.py b/poezio/config.py index 8da71071..4eb43cad 100644 --- a/poezio/config.py +++ b/poezio/config.py @@ -10,35 +10,37 @@ TODO: get http://bugs.python.org/issue1410680 fixed, one day, in order to remove our ugly custom I/O methods. """ +import logging import logging.config import os -import stat import sys -import pkg_resources from configparser import RawConfigParser, NoOptionError, NoSectionError from pathlib import Path -from shutil import copy2 -from typing import Callable, Dict, List, Optional, Union, Tuple +from typing import Dict, List, Optional, Union, Tuple, cast, Any -from poezio.args import parse_args from poezio import xdg +from slixmpp import JID, InvalidJID + +log = logging.getLogger(__name__) # type: logging.Logger ConfigValue = Union[str, int, float, bool] -DEFSECTION = "Poezio" +ConfigDict = Dict[str, Dict[str, ConfigValue]] + +USE_DEFAULT_SECTION = '__DEFAULT SECTION PLACEHOLDER__' -DEFAULT_CONFIG = { +DEFAULT_CONFIG: ConfigDict = { 'Poezio': { 'ack_message_receipts': True, 'add_space_after_completion': True, 'after_completion': ',', 'alternative_nickname': '', 'auto_reconnect': True, + 'autocolor_tab_names': False, 'autorejoin_delay': '5', 'autorejoin': False, 'beep_on': 'highlight private invite disconnect', - 'bookmark_on_join': False, 'ca_cert_path': '', 'certificate': '', 'certfile': '', @@ -50,7 +52,6 @@ DEFAULT_CONFIG = { 'custom_port': '', 'default_nick': '', 'default_muc_service': '', - 'deterministic_nick_colors': True, 'device_id': '', 'nick_color_aliases': True, 'display_activity_notifications': False, @@ -74,7 +75,6 @@ DEFAULT_CONFIG = { '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, @@ -92,6 +92,8 @@ DEFAULT_CONFIG = { 'lazy_resize': True, 'log_dir': '', 'log_errors': True, + 'mam_sync': True, + 'mam_sync_limit': 2000, 'max_lines_in_memory': 2048, 'max_messages_in_memory': 2048, 'max_nick_length': 25, @@ -133,9 +135,11 @@ DEFAULT_CONFIG = { 'show_useless_separator': True, 'status': '', 'status_message': '', + 'synchronise_open_rooms': True, 'theme': 'default', 'themes_dir': '', 'tmp_image_dir': '', + 'unique_prefix_tab_names': False, 'use_bookmarks_method': '', 'use_log': True, 'use_remote_bookmarks': True, @@ -157,21 +161,33 @@ DEFAULT_CONFIG = { } -class Config(RawConfigParser): +class PoezioConfigParser(RawConfigParser): + def optionxform(self, value) -> str: + return str(value) + + +class Config: """ load/save the config to a file """ - def __init__(self, file_name: Path, default=None) -> None: - RawConfigParser.__init__(self, None) + configparser: PoezioConfigParser + file_name: Path + default: ConfigDict + default_section: str = 'Poezio' + + def __init__(self, file_name: Path, default: Optional[ConfigDict] = None) -> None: + self.configparser = PoezioConfigParser() # make the options case sensitive - self.optionxform = lambda param: str(param) self.file_name = file_name self.read_file() - self.default = default + self.default = default or {} + + def optionxform(self, value): + return str(value) def read_file(self): - RawConfigParser.read(self, str(self.file_name), encoding='utf-8') + self.configparser.read(str(self.file_name), encoding='utf-8') # Check config integrity and fix it if it’s wrong # only when the object is the main config if self.__class__ is Config: @@ -182,38 +198,62 @@ class Config(RawConfigParser): def get(self, option: str, default: Optional[ConfigValue] = None, - section=DEFSECTION) -> ConfigValue: + section: str = USE_DEFAULT_SECTION) -> Any: """ 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 section == USE_DEFAULT_SECTION: + section = self.default_section if default is None: - if self.default: - default = self.default.get(section, {}).get(option) - else: - default = '' + default = self.default.get(section, {}).get(option, '') + res: Optional[ConfigValue] try: if isinstance(default, bool): - res = self.getboolean(option, section) + res = self.configparser.getboolean(section, option) elif isinstance(default, int): - res = self.getint(option, section) + res = self.configparser.getint(section, option) elif isinstance(default, float): - res = self.getfloat(option, section) + res = self.configparser.getfloat(section, option) else: - res = self.getstr(option, section) + res = self.configparser.get(section, option) except (NoOptionError, NoSectionError, ValueError, AttributeError): - return default if default is not None else '' + return default if res is None: return default return res + def _get_default(self, option, section): + if self.default: + return self.default.get(section, {}).get(option) + else: + return '' + + def sections(self, *args, **kwargs) -> List[str]: + return self.configparser.sections(*args, **kwargs) + + def options(self, *args, **kwargs): + return self.configparser.options(*args, **kwargs) + + def has_option(self, *args, **kwargs) -> bool: + return self.configparser.has_option(*args, **kwargs) + + def has_section(self, *args, **kwargs) -> bool: + return self.configparser.has_section(*args, **kwargs) + + def add_section(self, *args, **kwargs): + return self.configparser.add_section(*args, **kwargs) + + def remove_section(self, *args, **kwargs): + return self.configparser.remove_section(*args, **kwargs) + def get_by_tabname(self, option, - tabname: str, + tabname: JID, fallback=True, fallback_server=True, default=''): @@ -223,15 +263,12 @@ class Config(RawConfigParser): in the section, we search for the global option if fallback is True. And we return `default` as a fallback as a last resort. """ - from slixmpp import JID - if isinstance(tabname, JID): - tabname = tabname.full if self.default and (not default) and fallback: - default = self.default.get(DEFSECTION, {}).get(option, '') + default = self.default.get(self.default_section, {}).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) + return self.get(option, default, tabname.full) if fallback_server: return self.get_by_servname(tabname, option, default, fallback) if fallback: @@ -243,7 +280,10 @@ class Config(RawConfigParser): """ Try to get the value of an option for a server """ - server = safeJID(jid).server + try: + server = JID(jid).server + except InvalidJID: + server = '' if server: server = '@' + server if server in self.sections() and option in self.options(server): @@ -252,11 +292,13 @@ class Config(RawConfigParser): return self.get(option, default) return default - def __get(self, option, section=DEFSECTION, **kwargs): + def __get(self, option, section=USE_DEFAULT_SECTION, **kwargs): """ facility for RawConfigParser.get """ - return RawConfigParser.get(self, section, option, **kwargs) + if section == USE_DEFAULT_SECTION: + section = self.default_section + return self.configparser.get(section, option, **kwargs) def _get(self, section, conv, option, **kwargs): """ @@ -264,29 +306,54 @@ class Config(RawConfigParser): """ return conv(self.__get(option, section, **kwargs)) - def getstr(self, option, section=DEFSECTION): + def getstr(self, option, section=USE_DEFAULT_SECTION) -> str: """ get a value and returns it as a string """ - return self.__get(option, section) + if section == USE_DEFAULT_SECTION: + section = self.default_section + try: + return self.configparser.get(section, option) + except (NoOptionError, NoSectionError, ValueError, AttributeError): + return cast(str, self._get_default(option, section)) - def getint(self, option, section=DEFSECTION): + def getint(self, option, section=USE_DEFAULT_SECTION) -> int: """ get a value and returns it as an int """ - return RawConfigParser.getint(self, section, option) + if section == USE_DEFAULT_SECTION: + section = self.default_section + try: + return self.configparser.getint(section, option) + except (NoOptionError, NoSectionError, ValueError, AttributeError): + return cast(int, self._get_default(option, section)) - def getfloat(self, option, section=DEFSECTION): + def getfloat(self, option, section=USE_DEFAULT_SECTION) -> float: """ get a value and returns it as a float """ - return RawConfigParser.getfloat(self, section, option) + if section == USE_DEFAULT_SECTION: + section = self.default_section + try: + return self.configparser.getfloat(section, option) + except (NoOptionError, NoSectionError, ValueError, AttributeError): + return cast(float, self._get_default(option, section)) - def getboolean(self, option, section=DEFSECTION): + def getbool(self, option, section=USE_DEFAULT_SECTION) -> bool: """ get a value and returns it as a boolean """ - return RawConfigParser.getboolean(self, section, option) + if section == USE_DEFAULT_SECTION: + section = self.default_section + try: + return self.configparser.getboolean(section, option) + except (NoOptionError, NoSectionError, ValueError, AttributeError): + return cast(bool, self._get_default(option, section)) + + def getlist(self, option, section=USE_DEFAULT_SECTION) -> List[str]: + if section == USE_DEFAULT_SECTION: + section = self.default_section + return self.getstr(option, section).split(':') def write_in_file(self, section: str, option: str, value: ConfigValue) -> bool: @@ -382,8 +449,7 @@ class Config(RawConfigParser): if file_ok(self.file_name): try: with self.file_name.open('r', encoding='utf-8') as df: - lines_before = [line.strip() - for line in df] # type: List[str] + lines_before: List[str] = [line.strip() for line in df] except OSError: log.error( 'Unable to read the config file %s', @@ -393,7 +459,7 @@ class Config(RawConfigParser): else: lines_before = [] - sections = {} # type: Dict[str, List[int]] + sections: Dict[str, List[int]] = {} duplicate_section = False current_section = '' current_line = 0 @@ -420,7 +486,7 @@ class Config(RawConfigParser): return (sections, lines_before) def set_and_save(self, option: str, value: ConfigValue, - section=DEFSECTION) -> Tuple[str, str]: + section=USE_DEFAULT_SECTION) -> Tuple[str, str]: """ set the value in the configuration then save it to the file @@ -428,10 +494,12 @@ class Config(RawConfigParser): # 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 section == USE_DEFAULT_SECTION: + section = self.default_section + if isinstance(value, str) and value == "toggle": + current = self.getbool(option, section) if isinstance(current, bool): - value = str(not current) + value = str(not current).lower() else: if current.lower() == "false": value = "true" @@ -442,11 +510,12 @@ class Config(RawConfigParser): 'Could not toggle option: %s.' ' Current value is %s.' % (option, current or "empty"), 'Warning') + value = str(value) if self.has_section(section): - RawConfigParser.set(self, section, option, value) + self.configparser.set(section, option, value) else: self.add_section(section) - RawConfigParser.set(self, section, option, value) + self.configparser.set(section, option, value) if not self.write_in_file(section, option, value): return ('Unable to write in the config file', 'Error') if isinstance(option, str) and 'password' in option and 'eval_password' not in option: @@ -454,41 +523,47 @@ class Config(RawConfigParser): return ("%s=%s" % (option, value), 'Info') def remove_and_save(self, option: str, - section=DEFSECTION) -> Tuple[str, str]: + section=USE_DEFAULT_SECTION) -> Tuple[str, str]: """ Remove an option and then save it the config file """ + if section == USE_DEFAULT_SECTION: + section = self.default_section if self.has_section(section): - RawConfigParser.remove_option(self, section, option) + self.configparser.remove_option(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: str, value: ConfigValue, section=DEFSECTION): + def silent_set(self, option: str, value: ConfigValue, section=USE_DEFAULT_SECTION): """ Set a value, save, and return True on success and False on failure """ + if section == USE_DEFAULT_SECTION: + section = self.default_section if self.has_section(section): - RawConfigParser.set(self, section, option, value) + self.configparser.set(section, option, str(value)) else: self.add_section(section) - RawConfigParser.set(self, section, option, value) - return self.write_in_file(section, option, value) + self.configparser.set(section, option, str(value)) + return self.write_in_file(section, option, str(value)) - def set(self, option: str, value: ConfigValue, section=DEFSECTION): + def set(self, option: str, value: ConfigValue, section=USE_DEFAULT_SECTION): """ Set the value of an option temporarily """ + if section == USE_DEFAULT_SECTION: + section = self.default_section try: - RawConfigParser.set(self, section, option, value) + self.configparser.set(section, option, str(value)) except NoSectionError: pass - def to_dict(self) -> Dict[str, Dict[str, ConfigValue]]: + def to_dict(self) -> Dict[str, Dict[str, Optional[ConfigValue]]]: """ Returns a dict of the form {section: {option: value, option: value}, …} """ - res = {} # Dict[str, Dict[str, ConfigValue]] + res: Dict[str, Dict[str, Optional[ConfigValue]]] = {} for section in self.sections(): res[section] = {} for option in self.options(section): @@ -522,10 +597,10 @@ def file_ok(filepath: Path) -> bool: return bool(val) -def get_image_cache() -> Path: +def get_image_cache() -> Optional[Path]: if not config.get('extract_inline_images'): return None - tmp_dir = config.get('tmp_image_dir') + tmp_dir = config.getstr('tmp_image_dir') if tmp_dir: return Path(tmp_dir) return xdg.CACHE_HOME / 'images' @@ -564,43 +639,11 @@ def check_config(): print(' \033[31m%s\033[0m' % option) -def run_cmdline_args(): - "Parse the command line arguments" - global options - options = parse_args(xdg.CONFIG_HOME) - - # Copy a default file if none exists - if not options.filename.is_file(): - try: - options.filename.parent.mkdir(parents=True, exist_ok=True) - except OSError as e: - sys.stderr.write( - 'Poezio was unable to create the config directory: %s\n' % e) - sys.exit(1) - default = Path(__file__).parent / '..' / 'data' / 'default_config.cfg' - other = Path( - pkg_resources.resource_filename('poezio', 'default_config.cfg')) - if default.is_file(): - copy2(str(default), str(options.filename)) - elif other.is_file(): - copy2(str(other), str(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 options.filename.exists(): - options.filename.chmod(options.filename.stat().st_mode - | stat.S_IWUSR) - - global firstrun - firstrun = True - - -def create_global_config(): +def create_global_config(filename): "Create the global config object, or crash" try: global config - config = Config(options.filename, DEFAULT_CONFIG) + config = Config(filename, DEFAULT_CONFIG) except: import traceback sys.stderr.write('Poezio was unable to read or' @@ -609,11 +652,13 @@ def create_global_config(): sys.exit(1) -def setup_logging(): +def setup_logging(debug_file=''): "Change the logging config according to the cmdline options and config" global LOG_DIR LOG_DIR = config.get('log_dir') LOG_DIR = Path(LOG_DIR).expanduser() if LOG_DIR else xdg.DATA_HOME / 'logs' + from copy import deepcopy + logging_config = deepcopy(LOGGING_CONFIG) if config.get('log_errors'): try: LOG_DIR.mkdir(parents=True, exist_ok=True) @@ -621,8 +666,8 @@ def setup_logging(): # We can’t really log any error here, because logging isn’t setup yet. pass else: - LOGGING_CONFIG['root']['handlers'].append('error') - LOGGING_CONFIG['handlers']['error'] = { + logging_config['root']['handlers'].append('error') + logging_config['handlers']['error'] = { 'level': 'ERROR', 'class': 'logging.FileHandler', 'filename': str(LOG_DIR / 'errors.log'), @@ -630,37 +675,26 @@ def setup_logging(): } logging.disable(logging.WARNING) - if options.debug: - LOGGING_CONFIG['root']['handlers'].append('debug') - LOGGING_CONFIG['handlers']['debug'] = { + if debug_file: + logging_config['root']['handlers'].append('debug') + logging_config['handlers']['debug'] = { 'level': 'DEBUG', 'class': 'logging.FileHandler', - 'filename': options.debug, + 'filename': debug_file, 'formatter': 'simple', } logging.disable(logging.NOTSET) - if LOGGING_CONFIG['root']['handlers']: - logging.config.dictConfig(LOGGING_CONFIG) + if logging_config['root']['handlers']: + logging.config.dictConfig(logging_config) else: logging.disable(logging.ERROR) 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 poezio.common import safeJID as JID - global safeJID - safeJID = JID - LOGGING_CONFIG = { 'version': 1, - 'disable_existing_loggers': True, + 'disable_existing_loggers': False, 'formatters': { 'simple': { 'format': '%(asctime)s %(levelname)s:%(module)s:%(message)s' @@ -674,21 +708,8 @@ LOGGING_CONFIG = { } } -# 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 # type: Config - -# The logger object for this module -log = None # type: Optional[logging.Logger] - -# The command-line options -options = None - -# delayed import from common.py -safeJID = None # type: Optional[Callable] +# Global config object. Is setup for real in poezio.py +config = Config(Path('/dev/null')) # the global log dir LOG_DIR = Path() |