Index: main/waeup.sirp/trunk/src/waeup/sirp/utils/logger.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/utils/logger.py	(revision 6371)
+++ main/waeup.sirp/trunk/src/waeup/sirp/utils/logger.py	(revision 6371)
@@ -0,0 +1,310 @@
+##
+## logging.py
+## Login : <uli@pu.smp.net>
+## Started on  Mon Jun 13 01:25:07 2011 Uli Fouquet
+## $Id$
+## 
+## Copyright (C) 2011 Uli Fouquet
+## This program 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 2 of the License, or
+## (at your option) any later version.
+## 
+## This program 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 this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""
+Convenience stuff for logging.
+"""
+import os
+import grok
+import logging
+
+marker_ = object()
+
+def setup_logger(loggername, filename, site=marker_, logdir=marker_,
+                 handler=marker_, formatter=marker_, propagate=False,
+                 level=logging.DEBUG):
+    """Create a logger.
+    """
+    if site is marker_:
+        site = grok.getSite()
+    if '%s' in loggername:
+        name = getattr(site, '__name__', 'app')
+        loggername = loggername % (name,)
+    logger = logging.getLogger(loggername)
+    update_logger(logger, filename, site=site, logdir=logdir, handler=handler,
+                  formatter=formatter, propagate=propagate, level=level)
+    return logger, loggername
+
+def update_logger(logger, filename, site=marker_, logdir=marker_,
+                  handler=marker_, formatter=marker_, propagate=False,
+                  level=logging.DEBUG):
+    if site is marker_:
+        site = grok.getSite()
+    if logdir is marker_:
+        logdir = os.path.join(site['datacenter'].storage, 'logs')
+    if not os.path.exists(logdir):
+        os.mkdir(logdir)
+    filename = os.path.join(logdir, filename)
+
+    # Create a rotating file handler logger for datacenter.
+    if handler is marker_:
+        handler = logging.handlers.RotatingFileHandler(
+            filename, maxBytes=5*1024**1, backupCount=5)
+    if formatter is marker_:
+        formatter = logging.Formatter(
+            '%(asctime)s - %(levelname)s - %(message)s')
+    handler.setFormatter(formatter)
+
+    logger.propagate = propagate
+    logger.setLevel(level)
+    logger.addHandler(handler)
+    return logger
+
+def get_logger(loggername, filename, site=marker_, **kw):
+    """Get a logger.
+    """
+    if '%s' in loggername:
+        if site is marker_:
+            site = grok.getSite()
+        loggername = loggername % (
+            getattr(site, '__name__', 'app'))
+    logger = logging.getLogger(loggername)
+    if not logger.handlers:
+        kw.update(dict(site=site))
+        logger, loggername = setup_logger(
+            loggername, filename, **kw)
+    return logger
+
+class WAeUPLogger(object):
+
+    _logger = None
+
+    def __init__(self, loggername, filename, site=marker_, logdir=marker_,
+                 handlers=marker_, formatters=marker_, propagate=False,
+                 level=logging.DEBUG):
+        self.loggername = loggername
+        self.filename = filename
+        self.site = site
+        self.sitename = None
+        self.logdir = logdir
+        self.handlers = handlers
+        self.formatters = formatters
+        self.propagate = propagate
+        self.level = level
+        self._logger = None
+        return
+
+
+    @property
+    def logger(self):
+        if self._logger is not None:
+            if '__app' in self._logger.name:
+                if getattr(self.site, '__name__', None) is not None:
+                    self.setup()
+            return self._logger
+        self.setup()
+        return self._logger
+
+    def setup(self):
+        if self.site is marker_:
+            self.site = grok.getSite()
+        if '%s' in loggername:
+            name = getattr(site, '__name__', '__app')
+            loggername = loggername % (name,)
+        self.logger = logging.getLogger(loggername)
+        self.update()
+        return
+
+    def update(self):
+        if self.site is marker_:
+            self.site = grok.getSite()
+        if self.logdir is marker_:
+            self.logdir = os.path.join(site['datacenter'].storage, 'logs')
+        if not os.path.exists(self.logdir):
+            os.mkdir(self.logdir)
+        filename = os.path.join(self.logdir, self.filename)
+
+        # Create a rotating file handler logger for datacenter.
+        if self.handlers is marker_:
+            self.handlers = [logging.handlers.RotatingFileHandler(
+                    filename, maxBytes=5*1024**1, backupCount=5)]
+        if formatters is marker_:
+            formatters = [logging.Formatter(
+                    '%(asctime)s - %(levelname)s - %(message)s')]
+
+        self.logger.propagate = self.propagate
+        self.logger.setLevel(self.level)
+
+        for handler, formatter in map(lambda x, y: (x,y),
+                                     self.handlers, self.formatters):
+            handler.setFormatter(formatter)
+            self.logger.addHandler(handler)
+        return self.logger
+
+
+from zope import schema
+from zope.interface import Interface, Attribute
+
+class ISiteLoggers(Interface):
+
+    loggers = Attribute("A list or generator of registered WAeUPLoggers")
+
+    def register(name, filename=None, site=None, **options):
+        """Register a logger `name` which logs to `filename`.
+
+        If `filename` is not given, logfile will be `name` with
+        ``.log`` as filename extension.
+        """
+
+    def unregister(name):
+        """Unregister a once registered logger.
+        """
+
+class SiteLoggers(object):
+
+    grok.implements(ISiteLoggers)
+
+    @property
+    def loggers(self):
+        return self._loggers.values()
+
+    def __init__(self):
+        self._loggers = dict()
+
+    def getLogger(self, filename):
+        waeup_logger = self._loggers.get(filename, None)
+        return waeup_logger.logger
+
+    def register(self, filename, loggername=None, site=None, **kw):
+        if filename in self._loggers.keys():
+            raise KeyError('Logger "%s" exists already.' % filename)
+        if loggername is None:
+            loggername = filename
+            if '.' in filename:
+                loggername = filename.rplit('.', 1)[0]
+            loggername = 'waeup.sirp.%s.%s' % ('%s', loggername)
+        self._loggers[filename] = WAeUPLogger(
+            loggername, filename=filename, site=site, **kw)
+        return
+
+    def unregister(self, name):
+        if name in self._loggers.keys():
+            del self._loggers[name]
+        return
+
+class ILogger(Interface):
+    """A logger cares for setup, update and restarting of a Python logger.
+    """
+
+    logger = Attribute("""A :class:`logging.Logger` instance""")
+
+
+    def __init__(name, filename=None, site=None, **options):
+        """Create a WAeUP logger instance.
+        """
+        pass
+
+    def setup():
+        """Create a Python :class:`logging.Logger` instance.
+
+        The created logger is based on the params given by constructor.
+        """
+        pass
+
+    def update(**options):
+        """Update the logger.
+
+        Updates the logger respecting modified `options` and changed
+        paths.
+        """
+
+class WAeUPLogger(object):
+
+    grok.implements(ILogger)
+
+    _logger = None
+
+    def __init__(self, name, filename=None, site=None, **options):
+        self.name = name
+        self.filename = filename
+        if filename is None:
+            if '.' in name:
+                name = name.rsplit('.', 1)[1]
+            self.filename = "%s.log" % name
+        self.site = site
+        self.max_bytes = options.get('maxBytes', 5 * 1024 ** 2)
+        self.backup_count = options.get('backupCount', 5)
+        self.options = options
+        return
+
+    @property
+    def logger(self):
+        if self._needSetup():
+            self._setup()
+        return logging.getLogger(self._loggername)
+
+    def setup(self):
+        return self._setup()
+
+    def update(self, **options):
+        self._logger = None # unset to be set up again on next access.
+        # XXX: set options
+        pass
+
+    def _needSetup(self):
+        if self._logger is None:
+            return True
+        if not '__app' in self._logger.name:
+            return False
+        if getattr(self.site, '__name__', None) is not None:
+            return True
+        return False
+
+    def _removeHandlers(self):
+        handlers = self._logger.handlers
+        for handler in handlers:
+            self._logger.removeHandler(handler)
+
+    def _setup(self):
+        if self.site is None:
+            self.site = grok.getSite()
+        self._loggername = self.name
+        if not '.' in self._loggername:
+            self._loggername = 'waeup.sirp.%s.%s' % ('%s', self._loggername)
+        if '%s' in self._loggername:
+            site_name = getattr(self.site, '__name__', '__app')
+            if site_name is None:
+                site_name = '__app'
+            self._loggername = self._loggername % (site_name,)
+        self._logger = logging.getLogger(self._loggername)
+        return self._update()
+
+    def _update(self):
+        self._removeHandlers() # Remove any old handlers
+        self._logdir = os.path.join(self.site['datacenter'].storage, 'logs')
+        if not os.path.exists(self._logdir):
+            os.mkdir(self._logdir)
+        filename = os.path.join(self._logdir, self.filename)
+
+        # Create a rotating file handler logger for datacenter.
+        default_handler = logging.handlers.RotatingFileHandler(
+            filename, maxBytes=self.max_bytes, backupCount=self.backup_count)
+        self.handler = self.options.get('handler', default_handler)
+
+        default_formatter = logging.Formatter(
+            '%(asctime)s - %(levelname)s - %(message)s')
+        formatter = self.options.get('formatter', default_formatter)
+        self.handler.setFormatter(formatter)
+
+        self._logger.addHandler(self.handler)
+        self._logger.propagate = self.options.get('propagate', False)
+        self._logger.setLevel(self.options.get('level', logging.DEBUG))
+        return self._logger
Index: main/waeup.sirp/trunk/src/waeup/sirp/utils/tests/test_logger.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/utils/tests/test_logger.py	(revision 6371)
+++ main/waeup.sirp/trunk/src/waeup/sirp/utils/tests/test_logger.py	(revision 6371)
@@ -0,0 +1,207 @@
+##
+## test_logger.py
+## Login : <uli@pu.smp.net>
+## Started on  Mon Jun 13 14:40:04 2011 Uli Fouquet
+## $Id$
+## 
+## Copyright (C) 2011 Uli Fouquet
+## This program 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 2 of the License, or
+## (at your option) any later version.
+## 
+## This program 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 this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+import logging
+import os
+import re
+import shutil
+import tempfile
+from zope.app.testing.functional import FunctionalTestCase
+from zope.component.hooks import setSite, clearSite
+from zope.interface.verify import verifyClass, verifyObject
+from waeup.sirp.app import University
+from waeup.sirp.testing import FunctionalLayer, doctestsuite_for_module
+from waeup.sirp.utils.logger import (
+    ISiteLoggers, ILogger, SiteLoggers, WAeUPLogger)
+
+class FakeDatacenter(object):
+    def __init__(self):
+        self.storage = None
+
+class FakeSite(dict):
+    def __init__(self):
+        super(FakeSite, self).__init__()
+        self.__name__ = None
+        self['datacenter'] = FakeDatacenter()
+
+
+class SiteLoggersTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(SiteLoggersTests, self).setUp()
+
+        # Setup a sample site for each test
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+
+        self.workdir = tempfile.mkdtemp()
+        return
+
+    def tearDown(self):
+        super(SiteLoggersTests, self).tearDown()
+        shutil.rmtree(self.workdir)
+        shutil.rmtree(self.dc_root)
+        clearSite()
+        return
+
+    def test_iface(self):
+        loggers = SiteLoggers()
+        verifyClass(ISiteLoggers, SiteLoggers)
+        verifyObject(ISiteLoggers, loggers)
+        return
+
+    def test_loggers(self):
+        loggers = SiteLoggers()
+        loggers.register('sample1')
+        loggers.register('sample2')
+        result = loggers.loggers
+        assert len(result) == 2
+
+    def test_register(self):
+        loggers = SiteLoggers()
+        loggers.register('sample')
+        assert 'sample' in loggers._loggers.keys()
+
+    def test_register_duplicate(self):
+        loggers = SiteLoggers()
+        loggers.register('sample')
+        self.assertRaises(
+            KeyError,
+            loggers.register, 'sample')
+
+    def test_unregister(self):
+        loggers = SiteLoggers()
+        loggers.register('sample')
+        loggers.unregister('sample')
+        self.assertEqual(loggers._loggers, {})
+
+class LoggerTests(FunctionalTestCase):
+
+    layer = FunctionalLayer
+
+    def setUp(self):
+        super(LoggerTests, self).setUp()
+
+        # Setup a sample site for each test
+        app = University()
+        self.dc_root = tempfile.mkdtemp()
+        app['datacenter'].setStoragePath(self.dc_root)
+
+        # Prepopulate the ZODB...
+        self.getRootFolder()['app'] = app
+        self.app = self.getRootFolder()['app']
+
+        self.workdir = tempfile.mkdtemp()
+        return
+
+    def tearDown(self):
+        super(LoggerTests, self).tearDown()
+        shutil.rmtree(self.workdir)
+        shutil.rmtree(self.dc_root)
+        clearSite()
+        return
+
+    def test_iface(self):
+        setSite(self.app)
+        logger = WAeUPLogger('sample')
+        verifyClass(ILogger, WAeUPLogger)
+        verifyObject(ILogger, logger)
+        return
+
+    def test_logger_attr(self):
+        setSite(self.app)
+        logger = WAeUPLogger('sample')
+        raw_logger = logger.logger
+        assert isinstance(raw_logger, logging.Logger)
+
+    def test_site_name(self):
+        setSite(self.app)
+        logger = WAeUPLogger('waeup.sirp.%s.sample', site=self.app)
+        self.assertEqual(logger.logger.name, 'waeup.sirp.app.sample')
+
+    def test_site_no_name(self):
+        # While a site has no name, we get __app as placeholder
+        setSite(self.app)
+        self.app.__name__ = None
+        logger = WAeUPLogger('waeup.sirp.%s.sample', site=self.app)
+        self.assertEqual(logger.logger.name, 'waeup.sirp.__app.sample')
+
+    def test_site_changing_name(self):
+        # Changing the sitename (from None to a value) will be reflected
+        setSite(self.app)
+        self.app.__name__ = None
+        logger = WAeUPLogger('waeup.sirp.%s.sample', site=self.app)
+        self.assertEqual(logger.logger.name, 'waeup.sirp.__app.sample')
+        self.app.__name__ = 'myapp'
+        self.assertEqual(logger.logger.name, 'waeup.sirp.myapp.sample')
+        assert len(logger.logger.handlers) > 0
+
+    def test_loggername_expansion(self):
+        setSite(self.app)
+        logger = WAeUPLogger('sample', site=self.app)
+        logger.logger # Trigger setup
+        self.assertEqual(logger._loggername, 'waeup.sirp.app.sample')
+
+    def test_filename(self):
+        setSite(self.app)
+        logger = WAeUPLogger('sample')
+        logger.logger # Trigger setup
+        self.assertEqual(logger.filename, 'sample.log')
+
+    def test_filename_from_dotted_name(self):
+        setSite(self.app)
+        logger = WAeUPLogger('waeup.sirp.%s.sample')
+        logger.logger # Trigger setup
+        logfile = os.path.join(self.dc_root, 'logs', 'sample.log')
+        self.assertEqual(logger.filename, 'sample.log')
+        assert os.path.isfile(logfile)
+
+    def test_option_filename(self):
+        setSite(self.app)
+        logger = WAeUPLogger('sample', filename='my.log')
+        logger.logger # Trigger setup
+        logfile = os.path.join(self.dc_root, 'logs', 'my.log')
+        self.assertEqual(logger.filename, 'my.log')
+        assert os.path.isfile(logfile)
+
+    def test_option_propagate(self):
+        logger = WAeUPLogger('sample', site=self.app)
+        assert logger.logger.propagate is False  # default
+        logger = WAeUPLogger('sample', site=self.app, propagate=True)
+        assert logger.logger.propagate is True
+        logger = WAeUPLogger('sample', site=self.app, propagate=False)
+        assert logger.logger.propagate is False
+
+    def test_logging(self):
+        logger = WAeUPLogger('sample', site=self.app)
+        logger.logger.info('Hi there!')
+        logfile = os.path.join(self.dc_root, 'logs', 'sample.log')
+        assert os.path.isfile(logfile)
+        contents = open(logfile, 'rb').read()
+        self.assertTrue(re.match(
+                '^....-..-.. ..:..:..,... - INFO - Hi there!\n$', contents))
