<?xml version="1.0" encoding="UTF-8"?>
<commit>
  <added type="array">
    <added>
      <filename>COPYING.txt</filename>
    </added>
    <added>
      <filename>flashbake/console.py</filename>
    </added>
    <added>
      <filename>flashbake/plugins/mail.py</filename>
    </added>
    <added>
      <filename>test/test.py</filename>
    </added>
  </added>
  <modified type="array">
    <modified>
      <diff>@@ -11,6 +11,7 @@ and working directly on the project:
 * Ben Snider, bensnider.com - the original code for microblogs.py, docs
 * garthrk, github.org/garthrk - random fixes and tests, docs
 * Jason Penney, jasonpenney.net - brain storming, random fixes and enhancements
+* Tony Giunta - alternate implementation of uptime that uses uptime command
 
 I want to give a special thanks to Jonathan Coulton, Brad Turcotte and Beatnik
 Turtle for freely offering their songs for download.  Being able to grab a</diff>
      <filename>CREDITS.txt</filename>
    </modified>
    <modified>
      <diff>@@ -1,16 +1,31 @@
+#    copyright 2009 Thomas Gideon
 #
-#  __init__.py
-#  Shared classes and functions for the flashbake package.
-    
+#    This file is part of flashbake.
+#
+#    flashbake is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation, either version 3 of the License, or
+#    (at your option) any later version.
+#
+#    flashbake is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with flashbake.  If not, see &lt;http://www.gnu.org/licenses/&gt;.
+
+'''  __init__.py - Shared classes and functions for the flashbake package.'''
+
+from flashbake.plugins import PluginError, PLUGIN_ERRORS
+from types import *
+import commands
+import flashbake.plugins
+import glob
+import logging
 import os
 import os.path
-import logging
 import sys
-import commands
-import glob
-from types import *
-import flashbake.plugins
-from flashbake.plugins import PluginError, PLUGIN_ERRORS
 
 
 class ConfigError(Exception):
@@ -24,22 +39,54 @@ class ControlConfig:
         self.initialized = False
         self.extra_props = dict()
 
-        self.email = None
-        self.notice_to = None
-        self.notice_from = None
-        self.smtp_host = 'localhost'
-        self.smtp_port = 25
-        
         self.prop_types = dict()
-        self.prop_types['smtp_port'] = int
 
         self.plugin_names = list()
         self.msg_plugins = list()
-
         self.file_plugins = list()
+        self.notify_plugins = list()
 
         self.git_path = None
 
+
+    def capture(self, line):
+        ''' Parse a line from the control file if it is relevant to plugin configuration. '''
+        # grab comments but don't do anything
+        if line.startswith('#'):
+            return True
+
+        # grab blanks but don't do anything
+        if len(line.strip()) == 0:
+            return True
+
+        if line.find(':') &gt; 0:
+            prop_tokens = line.split(':', 1)
+            prop_name = prop_tokens[0].strip()
+            prop_value = prop_tokens[1].strip()
+
+            if 'plugins' == prop_name:
+               self.add_plugins(prop_value.split(','))
+               return True
+
+            # hang onto any extra propeties in case plugins use them
+            if not prop_name in self.__dict__:
+                self.extra_props[prop_name] = prop_value;
+                return True
+
+            try:
+                if prop_name in self.prop_types:
+                    prop_value = self.prop_types[prop_name](prop_value)
+                self.__dict__[prop_name] = prop_value
+            except:
+                raise ConfigError(
+                        'The value, %s, for option, %s, could not be parse as %s.'
+                        % (prop_value, prop_name, config.prop_types[prop_name]))
+
+            return True
+
+        return False
+
+
     def init(self):
         &quot;&quot;&quot; Do any property clean up, after parsing but before use &quot;&quot;&quot;
         if self.initialized == True:
@@ -47,17 +94,15 @@ class ControlConfig:
 
         self.initialized = True
 
-        if self.notice_from == None and self.notice_to != None:
-            self.notice_from = self.notice_to
-
         if len(self.plugin_names) == 0:
-            logging.debug('No plugins configured, enabling the stock set.')
             raise ConfigError('No plugins configured!')
 
+        all_plugins = list()
         for plugin_name in self.plugin_names:
             logging.debug(&quot;initalizing plugin: %s&quot; % plugin_name)
             try:
-                plugin = self.initplugin(plugin_name)
+                plugin = self.create_plugin(plugin_name)
+                all_plugins.append(plugin)
                 if isinstance(plugin, flashbake.plugins.AbstractMessagePlugin):
                     logging.debug(&quot;Message Plugin: %s&quot; % plugin_name)
                     if 'flashbake.plugins.location:Location' == plugin_name:
@@ -67,6 +112,9 @@ class ControlConfig:
                 if isinstance(plugin, flashbake.plugins.AbstractFilePlugin):
                     logging.debug(&quot;File Plugin: %s&quot; % plugin_name)
                     self.file_plugins.append(plugin)
+                if isinstance(plugin, flashbake.plugins.AbstractNotifyPlugin):
+                    logging.debug('Notify Plugin: %s' % plugin_name)
+                    self.notify_plugins.append(plugin)
             except PluginError, e:
                 # re-raise critical plugin error
                 if not e.reason == PLUGIN_ERRORS.ignorable_error:
@@ -75,7 +123,15 @@ class ControlConfig:
                 logging.warning('Skipping plugin, %s, ignorable error: %s' %
                         (plugin_name, e.name))
 
-    def sharedproperty(self, name, type = None):
+        for plugin in all_plugins:
+            plugin.share_properties(self)
+
+        for plugin in all_plugins:
+            plugin.capture_properties(self)
+            plugin.init(self)
+
+
+    def share_property(self, name, type=None):
         &quot;&quot;&quot; Declare a shared property, this way multiple plugins can share some
             value through the config object. &quot;&quot;&quot;
         if name in self.__dict__:
@@ -96,11 +152,11 @@ class ControlConfig:
 
         self.__dict__[name] = value
 
-    def addplugins(self, plugin_names):
+    def add_plugins(self, plugin_names):
         # use a comprehension to ensure uniqueness
         [self.__add_last(inbound_name) for inbound_name in plugin_names]
 
-    def initplugin(self, plugin_spec):
+    def create_plugin(self, plugin_spec):
         &quot;&quot;&quot; Initialize a plugin, including vetting that it meets the correct
             protocol; not private so it can be used in testing. &quot;&quot;&quot;
         if plugin_spec.find(':') &lt; 0:
@@ -118,31 +174,34 @@ class ControlConfig:
             raise PluginError(PLUGIN_ERRORS.unknown_plugin, plugin_spec)
 
         try:
-            # TODO re-visit pkg_resources, EntryPoint
             plugin_class = self.__forname(module_name, plugin_name)
             plugin = plugin_class(plugin_spec)
-        except:
+        except Exception, e:
+            logging.debug(e)
             logging.debug('Couldn\'t load class %s' % plugin_spec)
             raise PluginError(PLUGIN_ERRORS.unknown_plugin, plugin_spec)
         is_message_plugin = isinstance(plugin, flashbake.plugins.AbstractMessagePlugin)
         is_file_plugin = isinstance(plugin, flashbake.plugins.AbstractFilePlugin)
-        if not is_message_plugin and not is_file_plugin:
+        is_notify_plugin = isinstance(plugin, flashbake.plugins.AbstractNotifyPlugin)
+        if not is_message_plugin and not is_file_plugin and not is_notify_plugin:
             raise PluginError(PLUGIN_ERRORS.invalid_type, plugin_spec)
         if is_message_plugin:
             self.__checkattr(plugin_spec, plugin, 'connectable', bool)
             self.__checkattr(plugin_spec, plugin, 'addcontext', MethodType)
         if is_file_plugin:
             self.__checkattr(plugin_spec, plugin, 'processfiles', MethodType)
-
-        plugin.init(self)
+        if is_file_plugin:
+            self.__checkattr(plugin_spec, plugin, 'notify', MethodType)
 
         return plugin
 
+
     def __add_last(self, plugin_name):
         if plugin_name in self.plugin_names:
-            self.plugin_names.remove(plugin_name) 
+            self.plugin_names.remove(plugin_name)
         self.plugin_names.append(plugin_name)
 
+
     def __checkattr(self, plugin_spec, plugin, name, expected_type):
         try:
             attrib = eval('plugin.%s' % name)
@@ -152,6 +211,7 @@ class ControlConfig:
         if not isinstance(attrib, expected_type):
             raise PluginError(PLUGIN_ERRORS.invalid_attribute, plugin_spec, name)
 
+
     # with thanks to Ben Snider
     # http://www.bensnider.com/2008/02/27/dynamically-import-and-instantiate-python-classes/
     def __forname(self, module_name, plugin_name):
@@ -161,6 +221,7 @@ class ControlConfig:
         classobj = getattr(module, plugin_name)
         return classobj
 
+
 class HotFiles:
     &quot;&quot;&quot;
     Track the files as they are parsed and manipulated with regards to their git
@@ -214,7 +275,7 @@ class HotFiles:
                 self.control_files.add(rel_file)
             else:
                 self.linked_files[expanded_file] = link
-                
+
         if not file_exists:
             self.putabsent(filename)
 
@@ -262,10 +323,10 @@ class HotFiles:
 
         os.remove(message_file)
 
-    def needsnotice(self):
+    def needs_warning(self):
         return (len(self.not_exists) &gt; 0
                or len(self.linked_files) &gt; 0
-               or len(self.outside_files) &gt; 0) 
+               or len(self.outside_files) &gt; 0)
 
     def __check_link(self, filename):
         # add, above, makes sure filename is always relative
@@ -298,3 +359,17 @@ class HotFiles:
             return filepath.replace(prefix, &quot;&quot;)
         else:
             return os.path.relpath(filepath, prefix)
+
+
+def find_executable(executable):
+    found = filter(lambda ex: os.path.exists(ex),
+                   map(lambda path_token:
+                       os.path.join(path_token, executable),
+                       os.getenv('PATH').split(os.pathsep)))
+    if (len(found) == 0):
+        return None
+    return found[0]
+
+
+def executable_available(executable):
+    return find_executable(executable) != None</diff>
      <filename>flashbake/__init__.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,29 +1,36 @@
+#    copyright 2009 Thomas Gideon
 #
-#  commit.py
-#  Parses a project's control file and wraps git operations, calling the context
-#  script to build automatic commit messages as needed.
+#    This file is part of flashbake.
+#
+#    flashbake is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation, either version 3 of the License, or
+#    (at your option) any later version.
+#
+#    flashbake is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with flashbake.  If not, see &lt;http://www.gnu.org/licenses/&gt;.
+
+'''  commit.py - Parses a project's control file and wraps git operations, calling the context
+script to build automatic commit messages as needed.'''
 
-import os
-import sys
-import re
-import datetime
-import time
-import logging
-import context
-# this import is only valid for Linux
 import commands
-# Import smtplib for the actual sending function
-import smtplib
+import context
+import datetime
 import flashbake
 import git
+import logging
+import os
+import re
+import sys
+import time
 
-# Import the email modules we'll need
-if sys.hexversion &lt; 0x2050000:
-    from email.MIMEText import MIMEText
-else:
-    from email.mime.text import MIMEText
 
-def parsecontrol(project_dir, control_file, config = None, results = None):
+def parsecontrol(project_dir, control_file, config=None, results=None):
     &quot;&quot;&quot; Parse the dot-control file to get config options and hot files. &quot;&quot;&quot;
 
     logging.debug('Checking %s' % control_file)
@@ -32,7 +39,7 @@ def parsecontrol(project_dir, control_file, config = None, results = None):
         hot_files = flashbake.HotFiles(project_dir)
     else:
         hot_files = results
-        
+
     if None == config:
         control_config = flashbake.ControlConfig()
     else:
@@ -42,13 +49,13 @@ def parsecontrol(project_dir, control_file, config = None, results = None):
     try:
         for line in control_file:
             # skip anything else if the config consumed the line
-            if __capture(control_config, line):
+            if control_config.capture(line):
                 continue
 
             hot_files.addfile(line.strip())
     finally:
         control_file.close()
-        
+
     return (hot_files, control_config)
 
 def preparecontrol(hot_files, control_config):
@@ -58,7 +65,7 @@ def preparecontrol(hot_files, control_config):
         logging.debug(&quot;running plugin %s&quot; % plugin)
         plugin.processfiles(hot_files, control_config)
     return (hot_files, control_config)
-    
+
 def commit(control_config, hot_files, quiet_mins, dryrun):
     # change to the project directory, necessary to find the .flashbake file and
     # to correctly refer to the project files by relative paths
@@ -159,52 +166,16 @@ def commit(control_config, hot_files, quiet_mins, dryrun):
             commit_output = git_obj.commit(message_file, to_commit)
             logging.debug(commit_output)
         os.remove(message_file)
+        __send_commit_notice(control_config, to_commit)
         logging.info('Commit for known files complete.')
     else:
         logging.info('No changes to known files found to commit.')
 
-    if hot_files.needsnotice():
-        __sendnotice(control_config, hot_files)
+    if hot_files.needs_warning():
+        __send_warning(control_config, hot_files)
     else:
-        logging.info('No missing or untracked files found, not sending email notice.')
-        
-def __capture(config, line):
-    &quot;&quot;&quot; Used by the dot-control parsing code to capture files and properties
-        as they are encountered. &quot;&quot;&quot;
-    # grab comments but don't do anything
-    if line.startswith('#'):
-        return True
-
-    # grab blanks but don't do anything
-    if len(line.strip()) == 0:
-        return True
-
-    if line.find(':') &gt; 0:
-        prop_tokens = line.split(':', 1)
-        prop_name = prop_tokens[0].strip()
-        prop_value = prop_tokens[1].strip()
-
-        if 'plugins' == prop_name:
-           config.addplugins(prop_value.split(','))
-           return True
-
-        # hang onto any extra propeties in case plugins use them
-        if not prop_name in config.__dict__:
-            config.extra_props[prop_name] = prop_value;
-            return True
-
-        try:
-            if prop_name in config.prop_types:
-                prop_value = config.prop_types[prop_name](prop_value)
-            config.__dict__[prop_name] = prop_value
-        except:
-            raise flashbake.ConfigError(
-                    'The value, %s, for option, %s, could not be parse as %s.'
-                    % (prop_value, prop_name, config.prop_types[prop_name]))
-
-        return True
-
-    return False
+        logging.info('No missing or untracked files found, not sending warning notice.')
+
 
 def __trimgit(status_line):
     if status_line.find('-&gt;') &gt;= 0:
@@ -214,69 +185,22 @@ def __trimgit(status_line):
     tokens = status_line.split(':')
     return tokens[1].strip()
 
-def __sendnotice(control_config, hot_files):
-    if (None == control_config.notice_to
+
+def __send_warning(control_config, hot_files):
+    if (len(control_config.notify_plugins) == 0
             and not control_config.dryrun):
-        logging.info('Skipping notice, no notice_to: recipient set.')
+        logging.info('Skipping notice, no notify plugins configured.')
         return
 
-    body = ''
-    
-    if len(hot_files.not_exists) &gt; 0:
-        body += '\nThe following files do not exist:\n\n'
-
-        for file in hot_files.not_exists:
-           body += '\t' + file + '\n'
-
-        body += '\nMake sure there is not a typo in .flashbake and that you created/saved the file.\n'
-    
-    if len(hot_files.linked_files) &gt; 0:
-        body += '\nThe following files in .flashbake are links or have a link in their directory path.\n\n'
-
-        for (file, link) in hot_files.linked_files.iteritems():
-            if file == link:
-                body += '\t' + file + ' is a link\n'
-            else:
-                body += '\t' + link + ' is a link on the way to ' + file + '\n'
+    for plugin in control_config.notify_plugins:
+        plugin.warn(hot_files, control_config)
 
-        body += '\nMake sure the physical file and its parent directories reside in the project directory.\n'
-    
-    if len(hot_files.outside_files) &gt; 0:
-        body += '\nThe following files in .flashbake are not in the project directory.\n\n'
 
-        for file in hot_files.outside_files:
-           body += '\t' + file + '\n'
-
-        body += '\nOnly files in the project directory can be tracked and committed.\n'
-
-
-    if control_config.dryrun:
-        logging.debug(body)
-        if control_config.notice_to != None:
-            logging.info('Dry run, skipping email notice.')
+def __send_commit_notice(control_config, to_commit):
+    if (len(control_config.notify_plugins) == 0
+            and not control_config.dryrun):
+        logging.info('Skipping notice, no notify plugins configured.')
         return
 
-    # Create a text/plain message
-    msg = MIMEText(body, 'plain')
-
-    msg['Subject'] = ('Some files in %s do not exist'
-            % os.path.realpath(hot_files.project_dir))
-    msg['From'] = control_config.notice_from
-    msg['To'] = control_config.notice_to
-
-    # Send the message via our own SMTP server, but don't include the
-    # envelope header.
-    logging.debug('\nConnecting to SMTP on host %s, port %d'
-            % (control_config.smtp_host, control_config.smtp_port))
-
-    try:
-        s = smtplib.SMTP()
-        s.connect(host=control_config.smtp_host,port=control_config.smtp_port)
-        logging.info('Sending notice to %s.' % control_config.notice_to)
-        logging.debug(body)
-        s.sendmail(control_config.notice_from, [control_config.notice_to], msg.as_string())
-        logging.info('Notice sent.')
-        s.close()
-    except Exception, e:
-        logging.error('Couldn\'t connect, will send later.')
-        logging.debug(&quot;SMTP Error:\n&quot; + str(e));
+    for plugin in control_config.notify_plugins:
+        plugin.notify_commit(to_commit, control_config)</diff>
      <filename>flashbake/commit.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,6 +1,21 @@
+#    copyright 2009 Thomas Gideon
 #
-#  context.py
-#  Build up some descriptive context for automatic commit to git
+#    This file is part of flashbake.
+#
+#    flashbake is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation, either version 3 of the License, or
+#    (at your option) any later version.
+#
+#    flashbake is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with flashbake.  If not, see &lt;http://www.gnu.org/licenses/&gt;.
+
+'''  context.py - Build up some descriptive context for automatic commit to git'''
 
 import sys
 import os</diff>
      <filename>flashbake/context.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,7 +1,22 @@
+#    copyright 2009 Thomas Gideon
 #
-#  git.py
-#  Wrap the call outs to git, adding sanity checks and environment set up if
-#  needed.
+#    This file is part of flashbake.
+#
+#    flashbake is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation, either version 3 of the License, or
+#    (at your option) any later version.
+#
+#    flashbake is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with flashbake.  If not, see &lt;http://www.gnu.org/licenses/&gt;.
+
+'''  git.py - Wrap the call outs to git, adding sanity checks and environment set up if
+needed.'''
 
 import os
 import logging</diff>
      <filename>flashbake/git.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,5 +1,23 @@
-# from http://pypi.python.org/pypi/enum/
+#    copyright 2009 Thomas Gideon
+#
+#    This file is part of flashbake.
+#
+#    flashbake is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation, either version 3 of the License, or
+#    (at your option) any later version.
+#
+#    flashbake is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with flashbake.  If not, see &lt;http://www.gnu.org/licenses/&gt;.
+
 from enum import Enum
+import logging
+
 PLUGIN_ERRORS = Enum(
         'invalid_plugin',
         'invalid_type',
@@ -10,8 +28,9 @@ PLUGIN_ERRORS = Enum(
         'ignorable_error'
         )
 
+
 class PluginError(Exception):
-    def __init__(self, reason, plugin_spec, name = None):
+    def __init__(self, reason, plugin_spec, name=None):
         self.plugin_spec = plugin_spec
         self.reason = reason
         self.name = name
@@ -20,30 +39,73 @@ class PluginError(Exception):
             return '%s: %s' % (self.reason, self.plugin_spec)
         else:
             return '%s, %s: %s' % (self.plugin_spec, self.reason, self.name)
+
+
+def service_and_prefix(plugin_spec):
+    service_name = plugin_spec.split(':')[-1]
+    property_prefix = '_'.join(service_name.lower().strip().split(' '))
+    return service_name, property_prefix
+
+
 class AbstractPlugin():
     &quot;&quot;&quot; Common parent for all kinds of plugins, mostly to share option handling
         code. &quot;&quot;&quot;
+    def __init__(self, plugin_spec):
+        self.plugin_spec = plugin_spec
+        self.service_name, self.property_prefix = service_and_prefix(plugin_spec)
+        self.__property_defs = []
+        self.__shared_prop_defs = []
+
+
+    def define_property(self, name, type=None, required=False, default=None):
+        try:
+            self.__property_defs.append((name, type, required, default))
+        except AttributeError:
+            raise Exception('Call AbstractPlugin.__init__ in your plugin\'s __init__.')
+
+
+    def share_property(self, name, type=None, plugin_spec=None):
+        try:
+            if plugin_spec:
+                service_name, property_prefix = service_and_prefix(plugin_spec)
+                self.__shared_prop_defs.append(('%s_%s' % (property_prefix, name), type))
+            else:
+                self.__shared_prop_defs.append((name, type))
+        except AttributeError:
+            raise Exception('Call AbstractPlugin.__init__ in your plugin\'s __init__.')
+
+
+    def share_properties(self, config):
+        for name, type in self.__shared_prop_defs:
+            config.share_property(name, type)
+
+
+    def capture_properties(self, config):
+        try:
+            for prop in self.__property_defs:
+                assert len(prop) == 4, &quot;Property definition, %s, is invalid&quot; % (prop,)
+                self.__capture_property(config, *prop)
+        except AttributeError:
+            raise Exception('Call AbstractPlugin.__init__ in your plugin\'s __init__.')
+
+
     def init(self, config):
         &quot;&quot;&quot; This method is optional. &quot;&quot;&quot;
         pass
 
-    def requireproperty(self, config, name, type = None):
-        &quot;&quot;&quot; Useful to plugins to express a property that is required in the
-            dot-control file and to move it from the extra_props dict to a
-            property of the config. &quot;&quot;&quot;
-        if not name in config.extra_props:
-            raise PluginError(PLUGIN_ERRORS.missing_property, self.plugin_spec, name)
 
-        self.optionalproperty(config, name)
-
-    def optionalproperty(self, config, name, type = None):
+    def __capture_property(self, config, name, type=None, required=False, default=None):
         &quot;&quot;&quot; Move a property, if present, from the ControlConfig to the daughter
             plugin. &quot;&quot;&quot;
-        value = None
+        config_name = '%s_%s' % (self.property_prefix, name)
+        if required and not config_name in config.extra_props:
+            raise PluginError(PLUGIN_ERRORS.missing_property, self.plugin_spec, config_name)
+
+        value = default
 
-        if name in config.extra_props:
-            value = config.extra_props[name]
-            del config.extra_props[name]
+        if config_name in config.extra_props:
+            value = config.extra_props[config_name]
+            del config.extra_props[config_name]
 
         if type != None and value != None:
             try:
@@ -54,32 +116,50 @@ class AbstractPlugin():
                         % (value, name, type))
         self.__dict__[name] = value
 
-    def abstract(self): 
+
+    def abstract(self):
         &quot;&quot;&quot; borrowed this from Norvig
             http://norvig.com/python-iaq.html &quot;&quot;&quot;
         import inspect
         caller = inspect.getouterframes(inspect.currentframe())[1][3]
         raise NotImplementedError('%s must be implemented in subclass' % caller)
 
+
 class AbstractMessagePlugin(AbstractPlugin):
     &quot;&quot;&quot; Common parent class for all message plugins, will try to help enforce
         the plugin protocol at runtime. &quot;&quot;&quot;
-    def __init__(self, plugin_spec, connectable = False):
+    def __init__(self, plugin_spec, connectable=False):
+        AbstractPlugin.__init__(self, plugin_spec)
         self.connectable = connectable
-        self.plugin_spec = plugin_spec
+
 
     def addcontext(self, message_file, config):
         &quot;&quot;&quot; This method is required, it will asplode if not overridden by
             daughter classes. &quot;&quot;&quot;
         self.abstract()
 
+
 class AbstractFilePlugin(AbstractPlugin):
     &quot;&quot;&quot; Common parent class for all file plugins, will try to help enforce
         the plugin protocol at runtime. &quot;&quot;&quot;
-    def __init__(self, plugin_spec):
-        self.plugin_spec = plugin_spec
-
     def processfiles(self, hot_files, config):
         &quot;&quot;&quot; This method is required, it will asplode if not overridden by
             daughter classes. &quot;&quot;&quot;
         self.abstract()
+
+
+class AbstractNotifyPlugin(AbstractPlugin):
+    &quot;&quot;&quot; Common parent class for all notification plugins. &quot;&quot;&quot;
+    def warn(self, hot_files, config):
+        ''' Implementations will provide messages about the problem files in the
+            hot_files argument through different mechanisms.
+        
+            N.B. This method is required, it will asplode if not overridden by
+            daughter classes. '''
+        self.abstract()
+
+    
+    def notify_commit(self, to_commit):
+        ''' Option method to notify when a commit is performed, probably most useful
+            for services like desktop notifiers. '''
+        pass
\ No newline at end of file</diff>
      <filename>flashbake/plugins/__init__.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,44 +1,55 @@
+#    copyright 2009 Thomas Gideon
 #
-#  feed.py
-#  Stock plugin that pulls latest n items from a feed by a given author.
+#    This file is part of flashbake.
+#
+#    flashbake is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation, either version 3 of the License, or
+#    (at your option) any later version.
+#
+#    flashbake is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with flashbake.  If not, see &lt;http://www.gnu.org/licenses/&gt;.
+
+'''  feed.py - Stock plugin that pulls latest n items from a feed by a given author. '''
 
 import feedparser
 import logging
 from urllib2 import HTTPError, URLError
 from flashbake.plugins import AbstractMessagePlugin
 
+
+
 class Feed(AbstractMessagePlugin):
     def __init__(self, plugin_spec):
         AbstractMessagePlugin.__init__(self, plugin_spec, True)
-
-    def init(self, config):
-        &quot;&quot;&quot; Grab any extra properties that the config parser found and are
-            needed by this module. &quot;&quot;&quot;
-        self.requireproperty(config, 'feed_url')
-        self.optionalproperty(config, 'feed_author')
-        self.optionalproperty(config, 'feed_limit', int)
-        if self.feed_limit == None:
-            self.feed_limit = 5
+        self.define_property('url', required=True)
+        self.define_property('author')
+        self.define_property('limit', int, False, 5)
 
     def addcontext(self, message_file, config):
         &quot;&quot;&quot; Add the matching items to the commit context. &quot;&quot;&quot;
-        
+
         # last n items for m creator
-        (title,last_items) = self.__fetchfeed()
+        (title, last_items) = self.__fetchfeed()
 
         if len(last_items) &gt; 0:
-            if self.feed_author == None:
+            if self.author == None:
                 message_file.write('Last %(item_count)d entries from %(feed_title)s:\n'\
                     % {'item_count' : len(last_items), 'feed_title' : title})
             else:
                 message_file.write('Last %(item_count)d entries from %(feed_title)s by %(author)s:\n'\
-                    % {'item_count' : len(last_items), 'feed_title' : title, 'author': self.feed_author})
+                    % {'item_count' : len(last_items), 'feed_title' : title, 'author': self.author})
             for item in last_items:
               # edit the '%s' if you want to add a label, like 'Title %s' to the output
               message_file.write('%s\n' % item['title'])
               message_file.write('%s\n' % item['link'])
         else:
-            message_file.write('Couldn\'t fetch entries from feed, %s.\n' % self.feed_url)
+            message_file.write('Couldn\'t fetch entries from feed, %s.\n' % self.url)
 
         return len(last_items) &gt; 0
 
@@ -47,7 +58,7 @@ class Feed(AbstractMessagePlugin):
             creator. &quot;&quot;&quot;
 
         try:
-            feed = feedparser.parse(self.feed_url)
+            feed = feedparser.parse(self.url)
 
             if not 'title' in feed.feed:
                 logging.info('Feed title is empty, feed is either malformed or unavailable.')
@@ -57,13 +68,13 @@ class Feed(AbstractMessagePlugin):
 
             by_creator = []
             for entry in feed.entries:
-               if self.feed_author != None and entry.author != self.feed_author:
+               if self.author != None and entry.author != self.author:
                    continue
                title = entry.title
                title = title.encode('ascii', 'replace')
                link = entry.link
                by_creator.append({&quot;title&quot; : title, &quot;link&quot; : link})
-               if self.feed_limit &lt;= len(by_creator):
+               if self.limit &lt;= len(by_creator):
                    break
 
             return (feed_title, by_creator)</diff>
      <filename>flashbake/plugins/feed.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,17 +1,35 @@
-#  microblog.py
-#  by Ben Snider, bensnider.com
+#    copyright 2009 Ben Snider (bensnider.com), Thomas Gideon
+#
+#    This file is part of flashbake.
+#
+#    flashbake is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation, either version 3 of the License, or
+#    (at your option) any later version.
+#
+#    flashbake is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with flashbake.  If not, see &lt;http://www.gnu.org/licenses/&gt;.
+
+
+
+'''  microblog.py - microblog plugin by Ben Snider, bensnider.com '''
 
 import logging, urllib
 from urllib2 import HTTPError, URLError
 from xml.etree.ElementTree import ElementTree
 from flashbake.plugins import AbstractMessagePlugin
 
+
+
 class Twitter(AbstractMessagePlugin):
-    
+
     def __init__(self, plugin_spec):
         AbstractMessagePlugin.__init__(self, plugin_spec, True)
-        self.service_name = plugin_spec.split(':')[-1]
-        self.property_prefix = '_'.join(self.service_name.lower().strip().split(' '))
         self.service_url = 'http://twitter.com'
         self.optional_field_info = { \
             'source':{'path':'source', 'transform':propercase}, \
@@ -19,47 +37,33 @@ class Twitter(AbstractMessagePlugin):
             'favorited':{'path':'favorited', 'transform':propercase}, \
             'tweeted_on': {'path':'created_at', 'transform':utc_to_local}, \
         }
-        self.requiredproperties = [self.property_prefix+'_user']
-        self.optionalproperties = [self.property_prefix+'_limit', self.property_prefix+'_optional_fields']
-    
+        self.define_property('user', required=True)
+        self.define_property('limit', int, False, 5)
+        self.define_property('optional_fields')
+
+
     def init(self, config):
-        for prop in self.requiredproperties:
-            self.requireproperty(config, prop)
-        for prop in self.optionalproperties:
-            self.optionalproperty(config, prop)
-        
-        self.__formatproperties(config)
+        if self.limit &gt; 200:
+            logging.warn('Please use a limit &lt;= 200.');
+            self.limit = 200
+
         self.__setoptionalfields(config)
-        
+
         # simple user xml feed
-        self.twitter_url = '%(url)s/statuses/user_timeline/%(user)s.xml?count=%(limit)d' % \
-            {'url':self.service_url, \
-            'user':self.__dict__[self.property_prefix+'_user'], \
-            'limit':self.__dict__[self.property_prefix+'_limit']} \
-    
-    def __formatproperties(self, config):
-        limit_property_name = self.property_prefix+'_limit'
-        
-        # short circuit this conditional so we don't get a TypeError 
-        if (self.__dict__[limit_property_name] == None or int(self.__dict__[limit_property_name]) &lt; 1):
-            self.__dict__[limit_property_name] = 5
-        else:
-            self.__dict__[limit_property_name] = int(self.__dict__[limit_property_name])
-            # respect the twitter api limits
-            if (self.__dict__[limit_property_name] &gt; 200):
-                logging.warn('Please use a limit &lt;= 200.');
-                self.__dict__[limit_property_name] = 200
-    
+        self.twitter_url = '%(url)s/statuses/user_timeline/%(user)s.xml?count=%(limit)d' % {
+            'url':self.service_url,
+            'user':self.user,
+            'limit':self.limit}
+
+
     def __setoptionalfields(self, config):
-        optional_fields_name = self.property_prefix+'_optional_fields'
-        
         # We don't have to worry about a KeyError here since this property
         # should have been set to None by self.setoptionalproperty.
-        if (self.__dict__[optional_fields_name] == None):
-            self.__dict__[optional_fields_name] = []
+        if (self.optional_fields == None):
+            self.optional_fields = []
         else:
             # get the optional fields, split on commas
-            fields = self.__dict__[optional_fields_name].strip().split(',')
+            fields = self.optional_fields.strip().split(',')
             newFields = []
             for field in fields:
                 field = field.strip()
@@ -69,30 +73,31 @@ class Twitter(AbstractMessagePlugin):
                     newFields.append(field)
             # finally sort the list so its the same each run, provided the config is the same
             newFields.sort()
-            self.__dict__[optional_fields_name] = newFields
-        
+            self.optional_fields = newFields
+
+
     def addcontext(self, message_file, config):
         (title, last_tweets) = self.__fetchitems(config)
-        
+
         if (len(last_tweets) &gt; 0 and title != None):
             to_file = ('Last %(item_count)d %(service_name)s messages from %(twitter_title)s:\n' \
                 % {'item_count' : len(last_tweets), 'twitter_title' : title, 'service_name':self.service_name})
-            
-            i=1
+
+            i = 1
             for item in last_tweets:
                 to_file += ('%d) %s\n' % (i, item['tweet']))
-                for field in self.__dict__[self.property_prefix+'_optional_fields']:
+                for field in self.optional_fields:
                     to_file += ('\t%s: %s\n' % (propercase(field), item[field]))
-                i+=1
-            
+                i += 1
+
             logging.debug(to_file.encode('UTF-8'))
             message_file.write(to_file.encode('UTF-8'))
         else:
             message_file.write('Couldn\'t fetch entries from feed, %s.\n' % self.twitter_url)
 
         return len(last_tweets) &gt; 0
-    
-    
+
+
     def __fetchitems(self, config):
         ''' We fetch the tweets from the configured url in self.twitter_url,
         and return a list containing the formatted title and an array of
@@ -100,7 +105,7 @@ class Twitter(AbstractMessagePlugin):
         any optional fields. The 
         '''
         results = [None, []]
-        
+
         try:
             twitter_xml = urllib.urlopen(self.twitter_url)
         except HTTPError, e:
@@ -112,31 +117,31 @@ class Twitter(AbstractMessagePlugin):
         except IOError:
             logging.error('Socket error.')
             return results
-        
+
         tree = ElementTree()
         tree.parse(twitter_xml)
-        
+
         status = tree.find('status')
         if (status == None):
             return results
         # after this point we are pretty much guaranteed that we won't get an
         # exception or None value, provided the twitter xml stays the same
         results[0] = propercase(status.find('user/name').text)
-        
+
         for status in tree.findall('status'):
             tweet = {}
             tweet['tweet'] = status.find('text').text
-            for field in self.__dict__[self.property_prefix+'_optional_fields']:
+            for field in self.optional_fields:
                 tweet[field] = status.find(self.optional_field_info[field]['path']).text
                 if ('transform' in self.optional_field_info[field]):
                     tweet[field] = self.optional_field_info[field]['transform'](tweet[field])
             results[1].append(tweet)
-        
+
         return results
 
 
 class Identica(Twitter):
-    
+
     def __init__(self, plugin_spec):
         Twitter.__init__(self, plugin_spec)
         self.service_url = 'http://identi.ca/api'
@@ -150,7 +155,7 @@ def propercase(string):
     string = string.replace('_', ' ')
     string = string.title()
     return string
-    
+
 def utc_to_local(t):
     ''' ganked from http://feihonghsu.blogspot.com/2008/02/converting-from-local-time-to-utc.html '''
     import calendar, time, datetime</diff>
      <filename>flashbake/plugins/microblog.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,6 +1,21 @@
+#    copyright 2009 Thomas Gideon
 #
-#  music.py
-#  Stock plugin to calculate the system's uptime and add to the commit message.
+#    This file is part of flashbake.
+#
+#    flashbake is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation, either version 3 of the License, or
+#    (at your option) any later version.
+#
+#    flashbake is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with flashbake.  If not, see &lt;http://www.gnu.org/licenses/&gt;.
+
+'''  music.py - Plugin for gathering last played tracks from music player. '''
 
 import sqlite3
 import os.path
@@ -8,20 +23,17 @@ import logging
 import time
 from flashbake.plugins import AbstractMessagePlugin
 
+
+
 class Banshee(AbstractMessagePlugin):
-    def init(self, config):
+    def __init__(self, plugin_spec):
         &quot;&quot;&quot; Add an optional property for specifying a different location for the
-            Banshee database. &quot;&quot;&quot;   
-        self.optionalproperty(config, 'banshee_db')
-        self.optionalproperty(config, 'banshee_limit', int)
-        self.optionalproperty(config, 'banshee_last_played_format')
-        if self.banshee_db == None:
-            logging.debug('Using default location for Banshee database.')
-            self.banshee_db = os.path.join(os.path.expanduser('~'),
-                   '.config', 'banshee-1', 'banshee.db')
-        if self.banshee_limit == None:
-            logging.debug('Using default limit of 3 most recent tracks.')
-            self.banshee_limit = 3
+            Banshee database. &quot;&quot;&quot;
+        AbstractMessagePlugin.__init__(self, plugin_spec)
+        self.define_property('db', default=os.path.join(os.path.expanduser('~'), '.config', 'banshee-1', 'banshee.db'))
+        self.define_property('limit', int, default=3)
+        self.define_property('last_played_format')
+
 
     def addcontext(self, message_file, config):
         &quot;&quot;&quot; Open the Banshee database and query for the last played tracks. &quot;&quot;&quot;
@@ -31,8 +43,8 @@ from CoreTracks t
 join CoreArtists a on t.ArtistID = a.ArtistID
 order by LastPlayedStamp desc
 limit %d&quot;&quot;&quot;
-        query = query.strip() % self.banshee_limit
-        conn = sqlite3.connect(self.banshee_db)
+        query = query.strip() % self.limit
+        conn = sqlite3.connect(self.db)
         try:
             cursor = conn.cursor()
             logging.debug('Executing %s' % query)
@@ -41,9 +53,9 @@ limit %d&quot;&quot;&quot;
             message_file.write('Last %d track(s) played in Banshee:\n' % len(results))
             for result in results:
                 last_played = time.localtime(result[2])
-                if self.banshee_last_played_format != None:
-                    logging.debug('Using format %s' % self.banshee_last_played_format)
-                    last_played = time.strftime(self.banshee_last_played_format,
+                if self.last_played_format != None:
+                    logging.debug('Using format %s' % self.last_played_format)
+                    last_played = time.strftime(self.last_played_format,
                             last_played)
                 else:
                     last_played = time.ctime(result[2])</diff>
      <filename>flashbake/plugins/music.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,47 +1,56 @@
-# scrivener.py -- Scrivener flashbake plugin
-# by Jason Penney, jasonpenney.net
+#    copyright 2009 Jay Penney
+#
+#    This file is part of flashbake.
+#
+#    flashbake is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation, either version 3 of the License, or
+#    (at your option) any later version.
+#
+#    flashbake is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with flashbake.  If not, see &lt;http://www.gnu.org/licenses/&gt;.
+
+'''   scrivener.py - Scrivener flashbake plugin
+by Jason Penney, jasonpenney.net'''
 
-import logging, flashbake, flashbake.plugins, fnmatch, os
-import subprocess, glob
-import pickle
 from flashbake.plugins import *
+import logging
+import flashbake
+import flashbake.plugins
+import fnmatch
+import os
+import pickle
+import subprocess
+import glob
 
 
 
-## find_executable and executable_available could possibly be useful elsewhere
-def find_executable(executable):
-    found = filter(lambda ex: os.path.exists(ex),
-                   map(lambda path_token:
-                       os.path.join(path_token,executable),
-                       os.getenv('PATH').split(os.pathsep)))
-    if (len(found) == 0):
-        return None
-    return found[0]
-
-def executable_available(executable):
-    return find_executable(executable) != None
-
-    
 def find_scrivener_projects(hot_files, config, flush_cache=False):
     if flush_cache:
         config.scrivener_projects = None
-        
+
     if config.scrivener_projects == None:
         scrivener_projects = list()
         for f in hot_files.control_files:
-            if fnmatch.fnmatch(f,'*.scriv'):
+            if fnmatch.fnmatch(f, '*.scriv'):
                 scrivener_projects.append(f)
 
         config.scrivener_projects = scrivener_projects
 
     return config.scrivener_projects
 
+
 def _relpath(path, start):
     path = os.path.realpath(path)
     start = os.path.realpath(start)
     if not path.startswith(start):
         raise Error(&quot;unable to calculate paths&quot;)
-    if os.path.samefile(path,start):
+    if os.path.samefile(path, start):
         return &quot;.&quot;
 
     if not start.endswith(os.path.sep):
@@ -49,24 +58,25 @@ def _relpath(path, start):
 
     return path[len(start):]
 
-    
+
 def find_scrivener_project_contents(hot_files, scrivener_project):
-    contents=list()
-    for path, dirs, files in os.walk(os.path.join(hot_files.project_dir,scrivener_project)):        
+    contents = list()
+    for path, dirs, files in os.walk(os.path.join(hot_files.project_dir, scrivener_project)):
         if hasattr(os.path, &quot;relpath&quot;):
-            rpath = os.path.relpath(path,hot_files.project_dir)
+            rpath = os.path.relpath(path, hot_files.project_dir)
         else:
             try:
                 import pathutils
                 rpath = pathutils.relative(path, hot_files.project_dir)
             except:
                 rpath = _relpath(path, hot_files.project_dir)
-            
+
         for filename in files:
-            contents.append(os.path.join(rpath,filename))
+            contents.append(os.path.join(rpath, filename))
 
     return contents
 
+
 def get_logfile_name(scriv_proj_dir):
     return os.path.join(os.path.dirname(scriv_proj_dir),
                         &quot;.%s.flashbake.wordcount&quot; % os.path.basename(scriv_proj_dir))
@@ -74,41 +84,42 @@ def get_logfile_name(scriv_proj_dir):
 
 ## TODO: deal with deleted files
 class ScrivenerFile(AbstractFilePlugin):
-    
+
     def __init__(self, plugin_spec):
         AbstractFilePlugin.__init__(self, plugin_spec)
+        self.share_property('scrivener_projects')
 
-    def init(self,config):
-        config.sharedproperty('scrivener_projects')
-        
     def processfiles(self, hot_files, config):
         for f in find_scrivener_projects(hot_files, config):
             logging.debug(&quot;ScrivenerFile: adding '%s'&quot; % f)
-            for hotfile in find_scrivener_project_contents(hot_files,f):
+            for hotfile in find_scrivener_project_contents(hot_files, f):
                 #logging.debug(&quot; - %s&quot; % hotfile)
                 hot_files.control_files.add(hotfile)
 
+
 class ScrivenerWordcountFile(AbstractFilePlugin):
     &quot;&quot;&quot; Record Wordcount for Scrivener Files &quot;&quot;&quot;
     def __init__(self, plugin_spec):
         AbstractFilePlugin.__init__(self, plugin_spec)
+        self.share_property('scrivener_projects')
+        self.share_property('scrivener_project_count')
 
-    def init(self,config):
-        if not executable_available('textutil'):
+
+    def init(self, config):
+        if not flashbake.executable_available('textutil'):
             raise PluginError(PLUGIN_ERRORS.ignorable_error, self.plugin_spec, 'Could not find command, textutil.')
-        config.sharedproperty('scrivener_projects')
-        config.sharedproperty('scrivener_project_count')
-        
+
+
     def processfiles(self, hot_files, config):
         config.scrivener_project_count = dict()
-        for f in find_scrivener_projects(hot_files,config):
-            scriv_proj_dir  = os.path.join(hot_files.project_dir,f)
+        for f in find_scrivener_projects(hot_files, config):
+            scriv_proj_dir = os.path.join(hot_files.project_dir, f)
             hot_logfile = get_logfile_name(f)
-            logfile = os.path.join(hot_files.project_dir,hot_logfile)
+            logfile = os.path.join(hot_files.project_dir, hot_logfile)
 
             if os.path.exists(logfile):
                 logging.debug(&quot;logifile exists %s&quot; % logfile)
-                log = open(logfile,'r')
+                log = open(logfile, 'r')
                 oldCount = pickle.load(log)
                 log.close()
             else:
@@ -123,44 +134,44 @@ class ScrivenerWordcountFile(AbstractFilePlugin):
                 'Content': self.get_count(scriv_proj_dir, [&quot;*[0-9].rtfd&quot;]),
                 'Synopsis': self.get_count(scriv_proj_dir, ['*_synopsis.txt' ]),
                 'Notes': self.get_count(scriv_proj_dir, [ '*_notes.rtfd' ]),
-                'All':  self.get_count(scriv_proj_dir, ['*.rtfd','*.txt'])
+                'All':  self.get_count(scriv_proj_dir, ['*.rtfd', '*.txt'])
                 }
 
             config.scrivener_project_count[f] = { 'old': oldCount, 'new': newCount }
             if not config.context_only:
-                log = open(logfile,'w')
-                pickle.dump(config.scrivener_project_count[f]['new'],log)
+                log = open(logfile, 'w')
+                pickle.dump(config.scrivener_project_count[f]['new'], log)
                 log.close()
                 if not hot_logfile in hot_files.control_files:
                     hot_files.control_files.add(logfile)
 
 
 
-    def get_count(self,file,matches):
+    def get_count(self, file, matches):
         count = 0
-        args = ['textutil','-stdout','-cat', 'txt']
-        do_count=False
+        args = ['textutil', '-stdout', '-cat', 'txt']
+        do_count = False
         for match in list(matches):
-            for f in glob.glob(os.path.normpath(os.path.join(file,match))):
-                do_count=True
+            for f in glob.glob(os.path.normpath(os.path.join(file, match))):
+                do_count = True
                 args.append(f)
 
         if do_count:
             p = subprocess.Popen(args, stdout=subprocess.PIPE,
-                             close_fds = True)
+                             close_fds=True)
 
             for line in p.stdout:
                 count += len(line.split(None))
         return count
-    
+
+
 class ScrivenerWordcountMessage(AbstractMessagePlugin):
     &quot;&quot;&quot; Display Wordcount for Scrivener Files &quot;&quot;&quot;
 
-    def __init__(self,plugin_spec):
+    def __init__(self, plugin_spec):
         AbstractMessagePlugin.__init__(self, plugin_spec, False)
-        
-    def init(self, config):
-        config.sharedproperty('scrivener_project_counts')
+        self.share_property('scrivener_project_count')
+
 
     def addcontext(self, message_file, config):
         to_file = ''
@@ -171,12 +182,12 @@ class ScrivenerWordcountMessage(AbstractMessagePlugin):
                     new = config.scrivener_project_count[proj]['new'][key]
                     old = config.scrivener_project_count[proj]['old'][key]
                     diff = new - old
-                    to_file += &quot;- &quot; + key.ljust(10,' ') + str(new).rjust(20)
+                    to_file += &quot;- &quot; + key.ljust(10, ' ') + str(new).rjust(20)
                     if diff != 0:
                         to_file += &quot; (%+d)&quot; % (new - old)
-                    to_file +=&quot;\n&quot;
+                    to_file += &quot;\n&quot;
+
+            message_file.write(to_file)
 
-            message_file.write(to_file)        
 
-        
 </diff>
      <filename>flashbake/plugins/scrivener.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,14 +1,36 @@
+#    copyright 2009 Thomas Gideon
 #
-#  timezone.py
-#  Stock plugin to find the system's time zone add to the commit message.
+#    This file is part of flashbake.
+#
+#    flashbake is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation, either version 3 of the License, or
+#    (at your option) any later version.
+#
+#    flashbake is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with flashbake.  If not, see &lt;http://www.gnu.org/licenses/&gt;.
+
+
+'''  timezone.py - Stock plugin to find the system's time zone add to the commit message.'''
 
-import os, logging
 from flashbake.plugins import AbstractMessagePlugin
+import os
+import logging
+
+
+
+PLUGIN_SPEC='flashbake.plugins.timezone:TimeZone'
 
 class TimeZone(AbstractMessagePlugin):
-    def init(self, config):
-        &quot;&quot;&quot; Grab any extra properties that the config parser found and are needed by this module. &quot;&quot;&quot;
-        config.sharedproperty('timezone_tz')
+    def __init__(self, plugin_spec):
+        AbstractMessagePlugin.__init__(self, plugin_spec, False)
+        self.share_property('tz', plugin_spec=PLUGIN_SPEC)
+
 
     def addcontext(self, message_file, config):
         &quot;&quot;&quot; Add the system's time zone to the commit context. &quot;&quot;&quot;</diff>
      <filename>flashbake/plugins/timezone.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,16 +1,31 @@
+#    copyright 2009 Thomas Gideon
 #
-#  uptime.py
-#  Stock plugin to calculate the system's uptime and add to the commit message.
+#    This file is part of flashbake.
+#
+#    flashbake is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation, either version 3 of the License, or
+#    (at your option) any later version.
+#
+#    flashbake is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with flashbake.  If not, see &lt;http://www.gnu.org/licenses/&gt;.
+
+'''  uptime.py - Stock plugin to calculate the system's uptime and add to the commit message.'''
 
-import string
-import os.path
-import logging
 from flashbake.plugins import AbstractMessagePlugin
+from subprocess import Popen, PIPE
+import logging
+import os.path
+import string
+
 
-class UpTime(AbstractMessagePlugin):
-    def init(self, config):
-        &quot;&quot;&quot; Nothing needed. &quot;&quot;&quot;
 
+class UpTime(AbstractMessagePlugin):
     def addcontext(self, message_file, config):
         &quot;&quot;&quot; Add the system's up time to the commit context. &quot;&quot;&quot;
 
@@ -23,15 +38,15 @@ class UpTime(AbstractMessagePlugin):
 
         return True
 
+
     def __calcuptime(self):
         &quot;&quot;&quot; copied with blanket permission from
             http://thesmithfam.org/blog/2005/11/19/python-uptime-script/ &quot;&quot;&quot;
 
         if not os.path.exists('/proc/uptime'):
-            logging.warn('/proc/uptime doesn\'t exist.')
-            return None
+            return self.__run_uptime()
 
-        f = open( &quot;/proc/uptime&quot; )
+        f = open(&quot;/proc/uptime&quot;)
         try:
              contents = f.read().split()
         except:
@@ -42,24 +57,73 @@ class UpTime(AbstractMessagePlugin):
         total_seconds = float(contents[0])
 
         # Helper vars:
-        MINUTE  = 60
-        HOUR    = MINUTE * 60
-        DAY     = HOUR * 24
+        MINUTE = 60
+        HOUR = MINUTE * 60
+        DAY = HOUR * 24
 
         # Get the days, hours, etc:
-        days    = int( total_seconds / DAY )
-        hours   = int( ( total_seconds % DAY ) / HOUR )
-        minutes = int( ( total_seconds % HOUR ) / MINUTE )
-        seconds = int( total_seconds % MINUTE )
+        days = int(total_seconds / DAY)
+        hours = int((total_seconds % DAY) / HOUR)
+        minutes = int((total_seconds % HOUR) / MINUTE)
+        seconds = int(total_seconds % MINUTE)
 
         # Build up the pretty string (like this: &quot;N days, N hours, N minutes, N seconds&quot;)
         string = &quot;&quot;
-        if days&gt; 0:
-            string += str(days) + &quot; &quot; + (days == 1 and &quot;day&quot; or &quot;days&quot; ) + &quot;, &quot;
-        if len(string)&gt; 0 or hours&gt; 0:
-            string += str(hours) + &quot; &quot; + (hours == 1 and &quot;hour&quot; or &quot;hours&quot; ) + &quot;, &quot;
-        if len(string)&gt; 0 or minutes&gt; 0:
-            string += str(minutes) + &quot; &quot; + (minutes == 1 and &quot;minute&quot; or &quot;minutes&quot; ) + &quot;, &quot;
-        string += str(seconds) + &quot; &quot; + (seconds == 1 and &quot;second&quot; or &quot;seconds&quot; )
+        if days &gt; 0:
+            string += str(days) + &quot; &quot; + (days == 1 and &quot;day&quot; or &quot;days&quot;) + &quot;, &quot;
+        if len(string) &gt; 0 or hours &gt; 0:
+            string += str(hours) + &quot; &quot; + (hours == 1 and &quot;hour&quot; or &quot;hours&quot;) + &quot;, &quot;
+        if len(string) &gt; 0 or minutes &gt; 0:
+            string += str(minutes) + &quot; &quot; + (minutes == 1 and &quot;minute&quot; or &quot;minutes&quot;) + &quot;, &quot;
+        string += str(seconds) + &quot; &quot; + (seconds == 1 and &quot;second&quot; or &quot;seconds&quot;)
 
         return string
+
+
+    def __run_uptime(self):
+        &quot;&quot;&quot; For OSes that don't provide procfs, then try to use the updtime command.
+        
+            Thanks to Tony Giunta for this contribution. &quot;&quot;&quot;
+        if not flashbake.executable_available('uptime'):
+            return None
+
+        # Try to capture output of 'uptime' command, 
+        # if not found, catch OSError, log and return None
+        try:
+            output = Popen(&quot;uptime&quot;, stdout=PIPE).communicate()[0].split()
+        except OSError:
+            logging.warn(&quot;Can't find 'uptime' command in $PATH&quot;)
+            return None
+
+        # Parse uptime output string
+        # if len == 10 or 11, uptime is less than a day
+        if len(output) in [10, 11]:
+            days = &quot;00&quot;
+            hours_and_minutes = output[2].strip(&quot;,&quot;)
+        elif len(output) == 12:
+            days = output[2]
+            hours_and_minutes = output[4].strip(&quot;,&quot;)
+        else:
+            return None
+
+        # If time is exactly x hours/mins, no &quot;:&quot; in &quot;hours_and_minutes&quot; 
+        # and the interpreter will throw a ValueError
+        try:
+            hours, minutes = hours_and_minutes.split(&quot;:&quot;)
+        except ValueError:
+            if output[3].startswith(&quot;hr&quot;):
+                hours = hours_and_minutes
+                minutes = &quot;00&quot;
+            elif output[3].startwwith(&quot;min&quot;):
+                hours = &quot;00&quot;
+                minutes = hours_and_minutes
+            else:
+                return None
+
+        # Build up output string, might require Python 2.5+
+        uptime = (days + (&quot; day, &quot; if days == &quot;1&quot; else &quot; days, &quot;) +
+                hours + (&quot; hour, &quot; if hours == &quot;1&quot; else &quot; hours, &quot;) +
+                minutes + (&quot; minute&quot; if minutes == &quot;1&quot; else &quot; minutes&quot;))
+
+        return uptime
+</diff>
      <filename>flashbake/plugins/uptime.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,36 +1,52 @@
+#    copyright 2009 Thomas Gideon
 #
-#  weather.py
-#  Stock plugin for adding weather information to context, must have TZ or
-#  /etc/localtime available to determine city from ISO ID.
+#    This file is part of flashbake.
+#
+#    flashbake is free software: you can redistribute it and/or modify
+#    it under the terms of the GNU General Public License as published by
+#    the Free Software Foundation, either version 3 of the License, or
+#    (at your option) any later version.
+#
+#    flashbake is distributed in the hope that it will be useful,
+#    but WITHOUT ANY WARRANTY; without even the implied warranty of
+#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#    GNU General Public License for more details.
+#
+#    You should have received a copy of the GNU General Public License
+#    along with flashbake.  If not, see &lt;http://www.gnu.org/licenses/&gt;.
 
-import sys
-import urllib, urllib2
-import xml.dom.minidom
+'''  weather.py - Stock plugin for adding weather information to context, must have TZ or
+ /etc/localtime available to determine city from ISO ID. '''
+
+from flashbake.plugins import AbstractMessagePlugin
+from flashbake.plugins.timezone import findtimezone
+from urllib2 import HTTPError, URLError
+import logging
 import os
 import os.path
 import string
-import logging
-from urllib2 import HTTPError, URLError
-from flashbake.plugins.timezone import findtimezone
-from flashbake.plugins import AbstractMessagePlugin
+import sys
+import urllib
+import urllib2
+import xml.dom.minidom
+import timezone
+
+
 
 class Weather(AbstractMessagePlugin):
     def __init__(self, plugin_spec):
         AbstractMessagePlugin.__init__(self, plugin_spec, True)
-
-    def init(self, config):
-        &quot;&quot;&quot; Shares the timezone_tz: property with timezone:TimeZone and supports
-            an optional weather_city: property. &quot;&quot;&quot;
-        config.sharedproperty('timezone_tz')
-        self.optionalproperty(config, 'weather_city')
+        self.define_property('city')
+        self.share_property('tz', plugin_spec=timezone.PLUGIN_SPEC)
         ## plugin uses location_location from Location plugin
-        config.sharedproperty('location_location')
+        self.share_property('location_location')
+
 
     def addcontext(self, message_file, config):
         &quot;&quot;&quot; Add weather information to the commit message. Looks for
             weather_city: first in the config information but if that is not
             set, will try to use the system time zone to identify a city. &quot;&quot;&quot;
-        if config.location_location == None and self.weather_city == None:
+        if config.location_location == None and self.city == None:
             zone = findtimezone(config)
             if zone == None:
                 city = None
@@ -38,7 +54,7 @@ class Weather(AbstractMessagePlugin):
                 city = self.__parsecity(zone)
         else:
             if config.location_location == None:
-                city = self.weather_city
+                city = self.city
             else:
                 city = config.location_location
 </diff>
      <filename>flashbake/plugins/weather.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,29 +1,21 @@
 #!/usr/bin/env python
 #
 # setup.py for flashbake
-from setuptools import setup
+from setuptools import setup, find_packages
 
 setup(name='flashbake',
-        version='0.25',
+        version='0.26',
         author=&quot;Thomas Gideon&quot;,
         author_email=&quot;cmdln@thecommandline.net&quot;,
         url=&quot;http://thecommandline.net&quot;,
         license=&quot;GPLv3&quot;,
-        py_modules=['flashbake.commit',
-            'flashbake.context',
-            'flashbake.git',
-            'flashbake.plugins.feed',
-            'flashbake.plugins.timezone',
-            'flashbake.plugins.uptime',
-            'flashbake.plugins.weather',
-            'flashbake.plugins.microblog',
-            'flashbake.plugins.music',
-            'flashbake.plugins.location',
-            'flashbake.plugins.scrivener'
-            ],
+        packages=find_packages(exclude=['test.*']),
         install_requires='''
             enum &gt;=0.4.3
             feedparser &gt;=4.1
             ''',
-        scripts=['bin/flashbake',
-            'bin/flashbakeall'])
+        entry_points={
+                'console_scripts': [ 'flashbake = flashbake.console:main',
+                                     'flashbakeall = flashbake.console:multiple_projects' ]
+                }
+        )</diff>
      <filename>setup.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,6 +1,8 @@
-import unittest
 from flashbake import ControlConfig
 from flashbake.plugins import PluginError
+import logging
+import unittest
+
 
 class ConfigTestCase(unittest.TestCase):
     def setUp(self):
@@ -8,7 +10,7 @@ class ConfigTestCase(unittest.TestCase):
 
     def testinvalidspec(self):
         try:
-            plugin = self.config.initplugin('test.foo')
+            plugin = self.config.create_plugin('test.foo')
             self.fail('Should not be able to use unknown')
         except PluginError, error:
             self.assertEquals(str(error.reason), 'invalid_plugin',
@@ -16,7 +18,7 @@ class ConfigTestCase(unittest.TestCase):
 
     def testnoplugin(self):
         try:
-            plugin = self.config.initplugin('test.foo:Foo')
+            plugin = self.config.create_plugin('test.foo:Foo')
             self.fail('Should not be able to use unknown')
         except PluginError, error:
             self.assertEquals(str(error.reason), 'unknown_plugin',
@@ -25,7 +27,7 @@ class ConfigTestCase(unittest.TestCase):
     def testmissingparent(self):
         try:
             plugin_name = 'test.plugins:MissingParent'
-            plugin = self.config.initplugin(plugin_name)
+            plugin = self.config.create_plugin(plugin_name)
             self.fail('Should not have initialized plugin, %s' % plugin_name)
         except PluginError, error:
             reason = 'invalid_type'
@@ -58,7 +60,9 @@ class ConfigTestCase(unittest.TestCase):
                 'flashbake.plugins.timezone:TimeZone',
                 'flashbake.plugins.feed:Feed')
         for plugin_name in plugins:
-            plugin = self.config.initplugin(plugin_name)
+            plugin = self.config.create_plugin(plugin_name)
+            plugin.capture_properties(self.config)
+            plugin.init(self.config)
 
     def testnoauthorfail(self):
         &quot;&quot;&quot;Ensure that accessing feeds with no entry.author doesn't cause failures if the
@@ -70,7 +74,8 @@ class ConfigTestCase(unittest.TestCase):
 
     def testfeedfail(self):
         try:
-            self.config.initplugin('flashbake.plugins.feed:Feed')
+            plugin = self.config.create_plugin('flashbake.plugins.feed:Feed')
+            plugin.capture_properties(self.config)
             self.fail('Should not be able to initialize without full plugin props.')
         except PluginError, error:
             self.assertEquals(str(error.reason), 'missing_property',
@@ -81,13 +86,16 @@ class ConfigTestCase(unittest.TestCase):
         self.config.extra_props['feed_url'] = &quot;http://random.com/feed&quot;
 
         try:
-            self.config.initplugin('flashbake.plugins.feed:Feed')
+            plugin = self.config.create_plugin('flashbake.plugins.feed:Feed')
+            plugin.capture_properties(self.config)
         except PluginError, error:
             self.fail('Should be able to initialize with just the url.')
 
     def __testattr(self, plugin_name, name, reason):
         try:
-            plugin = self.config.initplugin(plugin_name)
+            plugin = self.config.create_plugin(plugin_name)
+            plugin.capture_properties(self.config)
+            plugin.init(self.config)
             self.fail('Should not have initialized plugin, %s' % plugin_name)
         except PluginError, error:
             self.assertEquals(str(error.reason), reason,</diff>
      <filename>test/config.py</filename>
    </modified>
    <modified>
      <diff>@@ -17,7 +17,7 @@ class NoConnectable(flashbake.plugins.AbstractMessagePlugin):
 
 class NoAddContext(flashbake.plugins.AbstractMessagePlugin):
     def __init__(self, plugin_spec):
-        self.connectable = True
+        flashbake.plugins.AbstractMessagePlugin.__init__(self, plugin_spec, True)
 
 class WrongConnectable(flashbake.plugins.AbstractMessagePlugin):
     def __init__(self, plugin_spec):</diff>
      <filename>test/plugins.py</filename>
    </modified>
  </modified>
  <removed type="array">
    <removed>
      <filename>LICENSE.txt</filename>
    </removed>
    <removed>
      <filename>bin/flashbake</filename>
    </removed>
    <removed>
      <filename>bin/flashbakeall</filename>
    </removed>
    <removed>
      <filename>bin/test</filename>
    </removed>
    <removed>
      <filename>flashbake/plugins/location.py</filename>
    </removed>
  </removed>
  <parents type="array">
    <parent>
      <id>3808bfaffd2490db3e73c4cf0452bccdc7182120</id>
    </parent>
    <parent>
      <id>af56ada0b95ca677bf6e10c052c91d2d109b880c</id>
    </parent>
  </parents>
  <author>
    <name>Jason Penney</name>
    <email>jpenney@jczorkmid.net</email>
  </author>
  <url>http://github.com/commandline/flashbake/commit/7d99eb4d2dc63b5cb1cd2d377ac8668390cb79ae</url>
  <id>7d99eb4d2dc63b5cb1cd2d377ac8668390cb79ae</id>
  <committed-date>2009-08-01T11:50:28-07:00</committed-date>
  <authored-date>2009-08-01T11:50:28-07:00</authored-date>
  <message>merged with upstream

Merge branch 'master' of git://github.com/commandline/flashbake

Conflicts:
	flashbake/plugins/location.py
	flashbake/plugins/timezone.py</message>
  <tree>e9047467682e70bd21d20e9eb60b80503f7fd2e2</tree>
  <committer>
    <name>Jason Penney</name>
    <email>jpenney@jczorkmid.net</email>
  </committer>
</commit>
