Commits

Marc-Alexandre Chan committed 5047806

Fixed config bugs: ConfigParser datatype, missing section, datetime.time conversion,
typos/syntax

  • Participants
  • Parent commits 9321b79

Comments (0)

Files changed (3)

File minibot/config.py

 a bad idea to store a copy of a configuration option which persists between run
 calls in an Event class. """
 
-from minibot.errors import ConfigValueError
+from minibot.util import DateParseMixin
+from minibot.errors import ConfigNameError, ConfigValueError,\
+                           CommandParameterError
 
-from ConfigParser import SafeConfigParser
+from ConfigParser import SafeConfigParser, RawConfigParser
+from os.path import abspath
 from logging import DEBUG, INFO, WARN, ERROR
 
 import re
 
 class Section(object):
     """ Abstract class representing a configuration section. Defines a generic
-    constructor that loads a dictionary, as well as a method to collapse the
-    current settings into a dictionary. Generally, __init__ should be overridden
-    by derived classes with the prototype __init__(self, values_dict), and
+    constructor that loads a zipped list, as well as a method to collapse the
+    current settings into zipped list. Generally, __init__ should be overridden
+    by derived classes with the prototype __init__(self, values_pairs), and
     should call Section's __init__ with its own default and allowed arguments
     (if applicable).
 
     Options are to be accessible within a section as an *attribute* of the
     section object. Hence, get() and set() are meant for use by derived classes
     for defining properties. """
-    def __init__(self, values_dict, default=dict(), allowed=None):
-        """ Load a ConfigParser.items()-style dictionary. Optionally pass a
-        dictionary containing default values and/or a list of allowed options.
-        """
-        self.options = {}
+    def __init__(self, values, default=list(), allowed=None):
+        """ Load a ConfigParser.items()-style zipped list. Optionally pass a
+        zipped list containing default values and/or a list of allowed options.
+        If an allowed list is specified, unallowed values are ignored. """
+        self._opts = dict()
+        self.allowed = allowed
         # first set defaults
-        for key, val in default.iteritems():
-            if (allowed is None or key in allowed) and val:
+        for key, val in default:
+            if (allowed is None or key in allowed):
                 self._opts[key] = val
 
         # then specified options to overwrite defaults if applicable
-        for key, val in values_dict.iteritems():
+        for key, val in values:
             if allowed is None or key in allowed:
                 self._opts[key] = val
 
     def items(self):
-        """ Return a ConfigParser.items()-style dictionary. """
-        return dict(self._opts)
+        """ Return a ConfigParser.items()-style zipped list. (For a dict, pass
+        the return value of items() to the dict() constructor).  """
+        return zip(self._opts.keys(), self._opts.values())
 
     def iteritems(self):
         """ Return a generator for the options in the section. """
     def set(self, name, value):
         """ Set a specified option `opt` to `value`. For use by derived classes.
         """
-        self._opts[name] = value
+        if self.allowed is None or name in self.allowed:
+            self._opts[name] = value
+        else:
+            raise ConfigNameError("Options '{}' not allowed".format(name))
 
     def verify(self):
         """ Verifies the current options. Must be defined by subclasses. """
         raise NotImplementedError()
 
 
-class SMinibot(Section):
+class SMinibot(Section, DateParseMixin):
     """ Represents the [minibot] section of the configuration file. """
-    def __init__(self, values_dict):
+    def __init__(self, values_pairs):
         defaults = {'salt' : 'Write a little every day!',
                     'user_agent' : 'DailyPromptMinibot/Default (no operator)',\
                     'refresh_rate' : 10,
                     'suggestions_day': 0,
                     'pidfile_path' : 'minibot.pid',
                     'pidfile_timeout' : 300}
-        super().__init__(values_dict, defaults)
+        super(self.__class__, self).__init__(values_pairs, defaults.items())
 
     @property
     def salt(self): return self.get('salt')
     @property
     def msg_chunk(self): return self.get('msg_chunk', int)
     @property
-    def default_time(self): return self.get('default_time')
+    def default_time(self):
+        return self.get('default_time', self._parse_time)
     @property
-    def suggestions_time(self): return self.get('suggestions_time')
+    def suggestions_time(self):
+        return self.get('suggestions_time', self._parse_time)
     @property
     def suggestions_day(self): return self.get('suggestions_day', int)
 
     @property
     def pidfile_timeout(self): return self.get('pidfile_timeout', int)
 
+    def _parse_time(self, time_str):
+        """ A time-only wrapper for DateParseMixin._parse_datetime(). """
+        return self._parse_datetime(None, time_str)
+
     def verify(self):
         """ Verifies the current options. Returns ConfigValueError or
         ValueError on invalid values. """
             raise ConfigValueError(
                 'config.minibot.msg_chunk must be positive: %d',
                 self.msg_rate)
-        time_re = re.compile('\d{1,2}:\d{1,2}(:\d{1,2})?')
-        if not time_re.match(self.default_time):
+        try:
+            self.default_time
+        except CommandParameterError:
             raise ConfigValueError("config.events.default_time must be in the "
                 "format HH:MM:SS: %s", self.default_time)
-        if not time_re.match(self.suggestions_time):
+        try:
+            self.suggestions_time
+        except CommandParameterError:
             raise ConfigValueError("config.events.suggestions_time must be in "
                 "the format HH:MM:SS: %s", self.default_time)
         if not 0 <= self.suggestions_day < 7:
                   'WARN'    : WARN,
                   'WARNING' : WARN,
                   'ERROR'   : ERROR,
+                  'ON'      : INFO,
+                  'OFF'     : WARN,
                   ''        : WARN} # default is WARN
 
     """ Represents the [log] section of the configuration file. """
-    def __init__(self, values_dict):
+    def __init__(self, values_pairs):
         defaults = {'file' : 'minibot.log',\
                     'db_file' : 'minibot-db.log',\
                     'format' : None,\
                     'date_format' : None,\
-                    'level' : levels_dict[''],\
-                    'db_level' : levels_dict['']}
-        super().__init__(values_dict, defaults)
+                    'level' : self.level_dict[''],\
+                    'db_level' : self.level_dict['']}
+        super(self.__class__, self).__init__(values_pairs, defaults.items())
 
     @property
     def file(self): return self.get('file')
     def level(self): return self.get('level', self._convert_level_string)
     @property
     def db_level(self): return self.get('db_level', self._convert_level_string)
+    @property
+    def format(self): return self.get('format')
+    @property
+    def date_format(self): return self.get('date_format')
 
     def _convert_level_string(self, value):
         """ Return a level value from its corresponding string, or the default
         logging value if the current value is invalid. """
-        return self.levels_dict.get(value.upper(), self.levels_dict[''])
+        return self.level_dict.get(value.upper(), self.level_dict[''])
 
     def verify(self):
         """ Verifies the current options. Returns ConfigValueError or
 
 class SSqlite(Section):
     """ Represents the [sqlite] section of the configuration file. """
-    def __init__(self, values_dict):
+    def __init__(self, values_pairs):
         defaults = {'file' : 'minibot.sql',\
                     'tableprefix' : ''}
-        super().__init__(values_dict, defaults)
+        super(self.__class__, self).__init__(values_pairs, defaults.items())
 
     @property
     def file(self): return self.get('file')
 
 class SReddit(Section):
     """ Represents the [reddit] section of the configuration file. """
+    def __init__(self, values_pairs):
+        defaults = { 'user' : '',
+                     'password' : '',
+                     'target' : ''}
+        super(self.__class__, self).__init__(values_pairs, defaults.items())
+
     @property
     def user(self): return self.get('user')
     @property
 
 class SUsers(Section):
     """ Represents the [users] section of the configuration file. """
-    def __init__(self, values_dict):
+    def __init__(self, values_pairs):
         """ Raises a ConfigValueError if any value cannot be interpreted as a
         base-10 int. """
-        self.users = {}
-        for key, val in values_dict.iteritems():
+        self._opts = {}
+        for key, val in values_pairs:
             try:
-                self.users[key] = int(val, 10)
+                self._opts[key] = int(val, 10)
             except ValueError:
                 raise ConfigValueError("User level for user '%s' is not "
                     "an integer: %s", key, val)
 
     def get_level(self, user):
-        return self.users[user]
+        return self._opts[user]
 
     def set_level(self, user, level):
-        self.users[user] = level
+        self._opts[user] = level
 
     def list(self):
-        return self.users.keys()
+        return self._opts.keys()
 
     def verify(self):
         """ Verifies the current options. Returns ConfigValueError or
 
     # A list of all sections expected in the configuration file and their
     # related classes.
-    SECTIONS = {'minibot' : SMinibot, 'sqlite' : SSqlite,\
-                'reddit'  : SReddit , 'users'  : SUsers}
+    SECTIONS = {'minibot' : SMinibot, 'log'    : SLog,
+                'reddit'  : SReddit , 'sqlite' : SSqlite,
+                'users'  : SUsers}
 
     def __init__(self, inifile='minibot.ini'):
         parser = SafeConfigParser()
-        self.file = inifile
-        with open(inifile) as fpIni:
+        self.file = abspath(inifile)
+        with open(self.file) as fpIni:
             parser.readfp(fpIni)
 
-        for section in SECTIONS.iterkeys():
+        for section in self.SECTIONS.iterkeys():
             if parser.has_section(section):
                 setattr(self, section,
-                    SECTIONS[section](parser.items(section, True)))
+                    self.SECTIONS[section](parser.items(section, True)))
             else:
-                setattr(self, section, SECTIONS[section](dict()))
+                setattr(self, section, self.SECTIONS[section](list()))
             # raises ConfigValueError/ValueError on failure
             getattr(self, section).verify()
 
     def verify(self):
         """ Verify the validity of the current configuration. Raises
         ConfigValueError or ValueError on errors; returns True otherwise. """
-        for section in SECTIONS.iterkeys():
+        for section in self.SECTIONS.iterkeys():
             getattr(self, section).verify()
         return True
 
         if filepath is None:
             filepath = self.file
 
-        with open(filepath) as fp_file:
+        with open(filepath, 'w') as fp_file:
             self.writefp(fp_file)
 
 
     def writefp(self, fp):
         """ Writes the current settings to a configuration file. ``fp`` is a
         file object. """
-        parser = SafeConfigParser()
-        for section in SECTIONS.iterkeys():
+        parser = RawConfigParser()
+        for section in self.SECTIONS.iterkeys():
             parser.add_section(section)
 
-            for opt, val in getattr(self, section).items():
+            for opt, val in getattr(self, section).iteritems():
                 parser.set(section, opt, val)
 
         parser.write(fp)

File minibot/errors.py

 # CONFIG
 class ConfigValueError(ValueError):
     """ Raised when an invalid configuration value is set or loaded from a file.
+    """
+
+class ConfigNameError(ValueError):
+    """ Raised when an invalid configuration field is set.
     """

File minibot/minibot.ini.default

 # Time to post suggestion thread
 suggestions_time = 00:00:00
 
+# Which day of the week to post suggestion threads on. 0 = Monday, etc.
+suggestions_day = 0
+
 # Path to the pidfile for use when running as a daemon, absolute or relative.
 # Must use a '.pid' extension. Default: 'minibot.pid'
 pidfile_path =
 # applicable if the minibot is run as a daemon. Default: 300.
 pidfile_timeout =
 
-[events]
-
-# Which day of the week to post suggestion threads on. 0 = Monday, etc.
-suggestions_day = 0
-
 [log]
 # Application log file. Default: 'minibot.log'
 file = minibot.log
 # Minimum level to log for database logs. Values: verbose, on, off. Default: off
 db_level = off
 
+# Format for log entries. See the Python library's logging.Formatter
+# documentation. This is for all logs. Default:
+# '%(asctime)s:%(name)s[%(process)d] %(levelname)s %(message)s'
+format =
+
+# Date/time format for log entries. See the Python library's logging.Formatter
+# documentation. This is for all logs. Default: ISO8601 format.
+date_format =
+
 [sqlite]
 file = minibot.sqlite
 tableprefix =
 
 # Login details for the bot's Reddit account.
 user =
-pass =
+password =
 
 
-# Format for log entries. See the Python library's logging.Formatter
-# documentation. This is for all logs. Default:
-# '%(asctime)s:%(name)s[%(process)d] %(levelname)s %(message)s'
-format =
-
-# Date/time format for log entries. See the Python library's logging.Formatter
-# documentation. This is for all logs. Default: ISO8601 format.
-date_format =
-
 # List of users and their permission levels, in the form username = permissions.
 # For the minibot, 100 means 'full access', and lower values means 'no access'.
 [users]