Commits

Anonymous committed 5bfba47

Simple `SELECT` and `FROM` clauses parsed correctly. Tested !

- Exception classes for all GViz error conditions have been
identified. All those related with GVizQL were formerly missing.
- Few more TODOs added.
- Refactorings applied in order to introduce `gvizql` module. Most
of the features needed to support GVizQL will be included in there.
- First commit of the architechture for GVizQL. Simple `SELECT` and
`FROM` clauses are parsed correctly (according to tests).
- Module `dutest` added for testing purposes.

  • Participants
  • Parent commits a73cd09
  • Branches gviz_ql
  • Tags deps_dutest

Comments (0)

Files changed (7)

File trac-dev/gviz/TODO

 - Allow data source providers to control the details needed to handle 
   queries.
 
+- GViz API QL: Parse more complex expressions in `select` clause 
+  (e.g. aggregate functions).
+
+- GViz API QL: Parse `WHERE` clause in detail ?
+

File trac-dev/gviz/__init__.py

     from search import *
     from timeline import *
     from vcs import *
-    from ig import *
+#    from ig import *
     msg = 'Ok'
 except Exception, exc:
 #    raise

File trac-dev/gviz/api.py

 from xmlrpclib import DateTime
 from datetime import datetime
 
+from gvizql import prepare_ql_data
+
 class GVizException(Exception):
   r"""Base class for all exception types defined in this package"""
 
     value has been assigned to a configuration option.
     """
 
+class GVizUnsupportedQueryOp(GVizException):
+    r"""Exception raised when an unsupported clause has been included 
+    in a GVizQL expression.
+    """
+
+class GVizInvalidQuery(GVizException):
+    r"""Exception raised on processing an invalid, or incorrect 
+    GVizQL expression.
+    """
+
+class GVizIllegalPattern(GVizException):
+    r"""Exception raised when an invalid, or incorrect formatting 
+    pattern has been specified by the end-user.
+    """
+
 def gviz_col(col_id, col_doc):
     r"""This function can be used to document the meaning of the 
     different columns in the table returned by GViz data sources as 
                 self.log.debug("IG: Custom parameters : %s", params)
                 table = None
                 try:
-                    data = provider.get_data(req, tq, **params)
-                    sch = provider.get_data_schema.im_func.func_code
-                    sch_args = (sch.co_argcount > 1) and (req,) or ()
-                    table = DataTable(provider.get_data_schema(*sch_args), \
-                                        data)
+#                    data = provider.get_data(req, tq, **params)
+#                    sch = provider.get_data_schema.im_func.func_code
+#                    sch_args = (sch.co_argcount > 1) and (req,) or ()
+                    dtargs = prepare_ql_data(provider, tq, req, **params)
+                    table = DataTable(*dtargs)
                     handler.output_response(req, table, None, \
                                             None, **std_params)
                 except RequestDone:

File trac-dev/gviz/gvizql.py

+#!/usr/bin/env python
+
+# Copyright 2009-2011 Olemis Lang <olemis at gmail.com>
+#
+#   Licensed under the Apache License, Version 2.0 (the "License");
+#   you may not use this file except in compliance with the License.
+#   You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+#   Unless required by applicable law or agreed to in writing, software
+#   distributed under the License is distributed on an "AS IS" BASIS,
+#   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#   See the License for the specific language governing permissions and
+#   limitations under the License.
+
+r"""Basic features needed to support Google Visualization Query Language
+(aka. GVizQL).
+
+Copyright 2009-2011 Olemis Lang <olemis at gmail.com>
+Licensed under the Apache License, Version 2.0 
+"""
+
+__all__ = 'prepare_ql_data', 'GVizQLClauseHandler', 'GVizQLParser', \
+          'defaultParser'
+__metaclass__ = type
+
+from re import compile
+
+#------------------------------------------------------
+#   GVizQL parsing and compilation
+#------------------------------------------------------
+
+class GVizQLParser:
+  r"""Objects used to parse GVizQL expressions. 
+  """
+  GVIZ_QL_RE = None
+  GVIZ_QL_TOKENS = {
+        'id' : '(?:[a-zA-Z]\w*|' # Simple identifiers \
+                  '`(?:\w|\s)*`)'   # Identifiers enclosed in backquotes,
+      }
+  updated = False
+  
+  @property
+  def pattern(self):
+    r"""Retrieve the regular expression describing the whole GVizQL 
+    syntax.
+    """
+    if not self.__class__.updated:
+      self.update_syntax()
+      self.__class__.updated = True
+    return self.GVIZ_QL_RE
+  @classmethod
+  def update_syntax(cls):
+    r"""Update the regular expression describing the whole GVizQL 
+    syntax.
+    """
+    patt = '\s*%s\s*$' % '(?:\s+|$|^)'.join( '(?:%s)?' %
+                                (ct.SYNTAX % cls.GVIZ_QL_TOKENS) \
+                                for ct in GVizQLClauseType.iterparse() \
+                                if ct.SYNTAX)
+    cls.GVIZ_QL_RE = compile(patt)
+  
+  def parse(self, tq):
+    r"""Parse and compile a GVizQL expression.
+    """
+    mo = self.pattern.match(tq)
+    if mo:
+      return GVizQLExpression(mo)
+    else:
+      return
+
+defaultParser = GVizQLParser()
+
+class GVizQLExpression:
+  r"""Compiled GVizQL expression.
+  """
+  def __init__(self, mo):
+    r"""Initialize a compiled GVizQL expression. 
+    
+    @param mo       the match made by the parser.
+    """
+    self.mo = mo
+
+#------------------------------------------------------
+#   GVizQL clause handlers 
+#------------------------------------------------------
+
+class GVizQLClauseType(type):
+  r"""Keep track of all GVizQL clause handlers installed in the 
+  system, as well as evaluation order.
+  
+  >>> ','.join(ct.get_props('keyw') for ct in GVizQLClauseType.iterparse())
+  'select,from,where,group by,pivot,order by,limit,offset,label,format,options'
+  >>> ','.join(ct.get_props('keyw') for ct in GVizQLClauseType.itereval())
+  'from,group by,pivot,where,order by,offset,limit,select,format,options,label'
+  """
+  CLAUSE_CACHE = dict()
+  SYNTAX_ORDER = list()
+  EVAL_ORDER = list()
+  PROPS = ('idx_syntax', 'idx_eval', 'keyw')
+  
+  def __new__(cls, name, bases, suite):
+    r"""Keep track of all GVizQL clause handlers installed in the 
+    system, as well as evaluation order.
+    """
+    try:
+      abstract = suite['__abstract__']
+      del suite['__abstract__']
+    except KeyError:
+      abstract = False
+    
+    @classmethod
+    def get_props(cls, propnm):
+      try:
+        return self._PROPS[propnm]
+      except KeyError, exc:
+        raise ValueError('Unsupported property %s', exc.message)
+    suite['get_props'] = get_props
+    
+    self = super(GVizQLClauseType, cls).__new__(cls, name, bases, suite)
+    if not abstract:
+      cnm = self.get_props('keyw')
+      GVizQLClauseType.CLAUSE_CACHE[cnm] = self
+      GVizQLClauseType.SYNTAX_ORDER.append(self)
+      GVizQLClauseType.SYNTAX_ORDER.sort(None, \
+                                    lambda x: x.get_props('idx_syntax'))
+      GVizQLClauseType.EVAL_ORDER.append(self)
+      GVizQLClauseType.EVAL_ORDER.sort(None, \
+                                    lambda x: x.get_props('idx_eval'))
+    GVizQLParser.updated = False
+    return self
+  
+  @staticmethod
+  def itereval():
+    r"""Iterate over GVizQL clause handlers following evaluation order.
+    """
+    return iter(GVizQLClauseType.EVAL_ORDER)
+  
+  @staticmethod
+  def iterparse():
+    r"""Iterate over GVizQL clause handlers following syntax order.
+    """
+    return iter(GVizQLClauseType.SYNTAX_ORDER)
+
+class GVizQLClauseHandler:
+  r"""Objects used to parse and retrieve items inside a clause and 
+  also responsible of performing the transformations dictated by this 
+  clause.
+  """
+  __metaclass__ = GVizQLClauseType
+  __abstract__ = True
+
+class GVizSelectClause(GVizQLClauseHandler):
+  _PROPS = {'idx_syntax' : 0, 'idx_eval': 7, 'keyw' : 'select'}
+  # TODO: Add more complex expressions
+  SYNTAX = r'select\ (?:\*|(?P<cols>%(id)s(?:,\s*%(id)s)*))'
+
+class GVizFromClause(GVizQLClauseHandler):
+  _PROPS = {'idx_syntax' : 1, 'idx_eval': 0, 'keyw' : 'from'}
+  SYNTAX = r'from\ (?P<basetable>%(id)s)'
+
+class GVizWhereClause(GVizQLClauseHandler):
+  _PROPS = {'idx_syntax' : 2, 'idx_eval': 3, 'keyw' : 'where'}
+  SYNTAX = r'where\ .*\s(?:%s)' % \
+              ('|'.join(GVizQLClauseType.CLAUSE_CACHE.keys()))
+
+class GVizGroupByClause(GVizQLClauseHandler):
+  _PROPS = {'idx_syntax' : 3, 'idx_eval': 1, 'keyw' : 'group by'}
+  SYNTAX = r''
+
+class GVizPivotClause(GVizQLClauseHandler):
+  _PROPS = {'idx_syntax' : 4, 'idx_eval': 2, 'keyw' : 'pivot'}
+  SYNTAX = r''
+
+class GVizOrderByClause(GVizQLClauseHandler):
+  _PROPS = {'idx_syntax' : 5, 'idx_eval': 4, 'keyw' : 'order by'}
+  SYNTAX = r''
+
+class GVizLimitClause(GVizQLClauseHandler):
+  _PROPS = {'idx_syntax' : 6, 'idx_eval': 6, 'keyw' : 'limit'}
+  SYNTAX = r''
+
+class GVizOffsetClause(GVizQLClauseHandler):
+  _PROPS = {'idx_syntax' : 7, 'idx_eval': 5, 'keyw' : 'offset'}
+  SYNTAX = r''
+
+class GVizLabelClause(GVizQLClauseHandler):
+  _PROPS = {'idx_syntax' : 8, 'idx_eval': 10, 'keyw' : 'label'}
+  SYNTAX = r''
+
+class GVizFormatClause(GVizQLClauseHandler):
+  _PROPS = {'idx_syntax' : 9, 'idx_eval': 8, 'keyw' : 'format'}
+  SYNTAX = r''
+
+class GVizOptionsClause(GVizQLClauseHandler):
+  _PROPS = {'idx_syntax' : 10, 'idx_eval': 9, 'keyw' : 'options'}
+  SYNTAX = r''
+
+#------------------------------------------------------
+#   Helper functions
+#------------------------------------------------------
+
+def prepare_ql_data(provider, tq, req, **params):
+  r"""Prepare the data and schema to be supplied to an instance of 
+  gviz_api.DataTable as determined by a GVizQL expression. This is 
+  accomplished by wrapping the original data with multiple iterators.
+  
+  @param provider   an instance of `IGVizDataProvider` interface 
+                    responsible for providing the base result set 
+                    (i.e. primary information) subsequently modified 
+                    by the application of the GVizQL expression.
+  @param tq         the GVizQL expression.
+  @param req        an object encapsulating the HTTP request/response 
+                    pair.
+  @param params     further parameters supported by `provider`.
+  @return           a binary tuple. The first item is the schema 
+                    describing the column names, data types, and 
+                    labels, as well as data structures from where 
+                    data will be retrieved. The second is the actual 
+                    data resulting from the application of the query 
+                    (quite often in the form of iterators).
+  """
+  sch = provider.get_data_schema.im_func.func_code
+  sch_args = (sch.co_argcount > 1) and (req,) or ()
+  if not tq:
+    data = provider.get_data(req, None, **params)
+    return (provider.get_data_schema(*sch_args), data)
+  else:
+    tq = defaultParser.parse(tq)
+    data = provider.get_data(req, tq, **params)
+    sch = provider.get_data_schema(*sch_args)
+    return tq.transform(sch, data, req)
+
+#------------------------------------------------------
+#   Global Testing
+#------------------------------------------------------
+
+__test__ = {
+  'Parser setup' : r"""
+      # >>> defaultParser.pattern.pattern
+      """,
+  'Parsing SELECT (simple)' : r"""
+      >>> parse('  select *  ')
+      <...GVizQLExpression object at ...>
+      >>> parse('select dept, salary  ')
+      <...GVizQLExpression object at ...>
+      >>> parse('select `email address`, name, `date`')
+      <...GVizQLExpression object at ...>
+      >>> parse('select lunchTime, name')
+      <...GVizQLExpression object at ...>
+      """,
+  'Parsing SELECT (complex)' : r"""
+      # >>> parse('select max(salary)')
+      # <...GVizQLExpression object at ...>
+      """,
+  'Parsing FROM' : r"""
+      >>> parse('  from employees') 
+      <...GVizQLExpression object at ...>
+      >>> parse('from `employees`')
+      <...GVizQLExpression object at ...>
+      >>> parse('select dept, salary from emp_data')
+      <...GVizQLExpression object at ...>
+      """,
+  }
+
+def test_suite():
+  from testing.dutest import MultiTestLoader, DocTestLoader
+  from unittest import defaultTestLoader
+  from doctest import ELLIPSIS
+  import sys
+  l = MultiTestLoader([defaultTestLoader, \
+                        DocTestLoader(
+                            extraglobs=dict(parse=defaultParser.parse),
+                            optionflags=ELLIPSIS
+                          )])
+  return l.loadTestsFromModule(sys.modules[__name__])
+

File trac-dev/gviz/proto.py

 from api import GVizDataNotModifiedError, GVizNotSupportedError, \
                 GVizUnknownProviderError, GVizDataNotModifiedError, \
                 GVizBadRequestError, GVizNotAuthenticatedError, \
-                GVizInvalidConfigError
+                GVizInvalidConfigError, GVizUnsupportedQueryOp, \
+                GVizInvalidQuery, GVizIllegalPattern
 
 JSON_MIME_TYPE = 'text/plain'
 
                             ['invalid_request', 'Bad request'],
                     GVizInvalidConfigError : \
                             ['internal_error', 'Invalid configuration'],
+                    GVizUnsupportedQueryOp : \
+                            ['unsupported_query_operation', \
+                              'Unsupported query operation'],
+                    GVizInvalidQuery : \
+                            ['invalid_query', 'Invalid query'],
+                    GVizIllegalPattern : \
+                            ['illegal_formatting_patterns', \
+                              'Invalid pattern'],
                    }
     
     def _error(self, req, error, reqId, version, responseHandler):

File trac-dev/gviz/testing/__init__.py

Empty file added.

File trac-dev/gviz/testing/dutest.py

+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+
+r"""An object-oriented API to test doctests using unittest runners.
+
+Module providing classes which extend doctest module so
+as to achieve better integration with unittest.
+
+It is different from the Pyhton 2.4 doctest unittest API because:
+
+  * A new unitest.TestLoader descendant now allows to load instances
+    of TestCases for doctests using unittest-style, supports building
+    complex test suites in a more natural way, and eases the use of
+    specialized instances of TestCase built out of doctest examples.
+  
+  * Other loaders allow users to extract TestCase instances out of 
+    TestCase descendants and doctests (and any other format) in a 
+    single step.
+    
+  * In this case unittest.TestResult instances report whether
+    individual examples have been successfully executed, or otherwise
+    have failed or raised an unexpected exception. Formerly TestResult
+    objects contained the whole report outputted by doctest module.
+    
+  * Test analysis require no further parsing to retrieve detailed
+    information about failures.
+  
+  * A whole new unittest API for doctest adds object orientation and
+    eliminates functions with big signatures.
+  
+  * It is not necessary to use DocTestRunner output streams in order
+    to collect test results.
+  
+  * A new hierarchy of doctest TestCases is now 
+    possible so for example, setUp and tearDown may
+    be redefined across a hierarchy of TestCases 
+    instead of providing this methods as parameters to
+    a function (breaking OOP philosophy and logic); or
+    maybe even failures and errors can be represented in a
+    custom way.
+  
+  * Allows to perform regression testing over tests written
+    using doctest.
+    
+  * Fixes a minor bug related with specifying different verbosity
+    levels from the command line to unittest.TestProgram (alias main).
+    
+  * Loads by default test cases for doctests plus those
+    formerly loaded by unittest.TestLoader
+
+It is similar to the Pyhton 2.4 doctest unittest API because:
+
+  * Provides integration with TestProgram and unittest test runners.
+    
+  * Allows to parameterize doctest behavior via doctest options
+
+
+A fuller explanation can be found in the following article:
+
+    "Doctest and unittest... now they'll live happily together", O. Lang
+    (2008) The Python Papers, Volume 3, Issue 1, pp. 31:51
+
+
+Note: The contents of this module were first implemented by the module
+oop.utils.testing contained in `PyOOP package`_.
+
+.. _PyOOP package: http://pypi.python.org/pypi/PyOOP
+
+"""
+
+# Copyright (C) 2008-2012 Olemis Lang
+# 
+# This module is free software, and you may redistribute it and/or modify
+# it under the same terms as Python itself, so long as this copyright message
+# and disclaimer are retained in their original form.
+# 
+# IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
+# SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
+# THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+# DAMAGE.
+# 
+# THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+# PARTICULAR PURPOSE.  THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
+# AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
+# SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+
+__all__ = 'DocTestCase', 'DocTestSuite', 'DocTestLoader', 'main', \
+          'defaultTestLoader', 'defaultTestRunner', \
+          'PackageTestLoader', 'MultiTestLoader'
+
+#------------------------------------------------------
+#    unittest interface to doctest module
+#------------------------------------------------------
+
+from sys import stderr, modules
+
+import doctest
+from unittest import TestCase
+import unittest
+
+from inspect import ismodule
+from StringIO import StringIO
+import re
+import os
+
+
+class _Doc2UnitTestRunner(doctest.DocTestRunner):
+    r"""An adapter class which allows to invoke transparently a
+    doctest runner from inside a unittest run. Besides it reports
+    the match made for each Example instance into separate
+    TestResult objects.
+    
+    Note: Users should not use this class directly. It is present
+            here as an implementation detail.
+    """
+    def __init__(self, checker=None, verbose=None, optionflags=0,
+                 result= None):
+        doctest.DocTestRunner.__init__(self, checker, verbose,
+                                       optionflags)
+        self.result= result
+    def summarize(verbose= None): 
+        pass
+    def run(self, test, compileflags=None, out=None, 
+            clear_globs=True):
+        doctest.DocTestRunner.run(self, test, compileflags, out,
+                                  clear_globs)
+    
+    def __cleanupTC(self, tc, result): pass
+                
+    def report_start(self, out, test, example):
+        result= self.result
+        if not result or result.shouldStop:
+            return
+        tc= example.tc
+        tc.ok= True
+        test.globs['__tester__']= tc
+        result.startTest(tc)
+        try:
+            tc.setUp()
+        except KeyboardInterrupt:
+            raise
+        except:
+            result.addError(tc, tc._exc_info())
+            tc.ok= False
+    def report_success(self, out, test, example, got):
+        tc= example.tc
+        if not tc.ok:        # example is Ok but setUp failed
+            return
+        result= self.result
+        if (not result) or result.shouldStop:
+            return
+        try:
+            tc.tearDown()
+        except KeyboardInterrupt:
+            raise
+        except:
+            result.addError(tc, tc._exc_info())
+        else:
+            result.addSuccess(tc)
+    def report_failure(self, out, test, example, got):
+        tc= example.tc
+        if not tc.ok:        # example is Ok but setUp failed
+            return
+        result= self.result
+        if not result or result.shouldStop:
+            return
+        msg = 'Example expected\n%s \n...but test outputted...\n%s'% \
+                                                (example.want, got)
+        try:
+            tc.ok= False
+            buff = StringIO()
+            doctest.DocTestRunner.report_failure(self, buff.write, 
+                                                 test, example, got)
+            
+            msg = buff.getvalue().split('\n', 2)[2]
+            buff.close()
+            buff = None
+        finally:
+            try:
+                tc.tearDown()
+            except KeyboardInterrupt:
+                raise
+            except:
+                # Errors take precedence over failures
+                result.addError(tc, tc._exc_info())
+            else:
+                result.addFailure(tc, (tc.failureException, 
+                        msg,
+                        None))
+    def report_unexpected_exception(self, out, test, example, 
+                                    exc_info):
+        tc= example.tc
+        if not tc.ok:        # example is Ok but setUp failed
+            return
+        result= self.result
+        if not result or result.shouldStop:
+            return
+        try:
+            tc.ok= False
+            tc.tearDown()
+        except KeyboardInterrupt:
+            raise
+        except:
+            if issubclass(exc_info[0], tc.failureException):
+                # Report faulty tearDown only if failure was detected
+                result.addError(tc, tc._exc_info())
+            else:
+                result.addError(tc, exc_info)
+        else:
+            if issubclass(exc_info[0], tc.failureException):
+                result.addFailure(tc, exc_info)
+            else:
+                result.addError(tc, exc_info)
+
+class DocTestCase(unittest.TestCase):
+    r"""A class whose instances represent tests over DocTest 
+    instance's examples.
+    """
+    def __init__(self, dt, idx=0):
+        r"""Create an instance that will test a DocTest instance's
+        example (the idx-th according to its examples member)
+        """
+        self.ok= True
+        self._dt= dt
+        ex= dt.examples[idx]
+        self._ex= ex
+        ex.tc= self
+        self._testMethodDoc= '%s (line %s)'% (dt.name, 
+                self.lineno is not None and self.lineno or '?')
+    
+    @property
+    def lineno(self):
+        if self._dt.filename:
+            if self._dt.lineno is not None and \
+                                         self._ex.lineno is not None:
+                return self._dt.lineno + self._ex.lineno + 1
+            elif self._ex.lineno is not None:
+                return self._ex.lineno+ 1
+            else:
+                return None
+        else:
+            return self._ex.lineno+ 1
+    
+    @property
+    def _testMethodName(self):
+        return "%s line %s"% (self._dt.name, self.lineno)
+    
+    def defaultTestResult(self):
+        return TestResult()
+    
+    def id(self):
+        return "Test "+ self._methodName
+    
+    def __repr__(self):
+        return "<%s test=%s line=%s>"% \
+                                (unittest._strclass(self.__class__), 
+                                self._dt.name, self.lineno)
+    
+    def run(self, result=None):
+        raise NotImplementedError, "doctest module doesn't allow to "\
+                                   "test single examples"
+    
+    def debug(self):
+        r"""Run the test without collecting errors in a TestResult
+        """
+        raise NotImplementedError, "doctest module doesn't allow to "\
+                                   "test single examples"
+
+class DocTestSuite(unittest.TestSuite):
+    r"""This test suite consists of DocTestCases derived from a single
+    DocTest instance.
+    """
+    docRunnerClass= _Doc2UnitTestRunner
+    docTestCaseClass= DocTestCase
+    def __init__(self, dt, optionflags=0, checker=None, runopts=None):
+        if runopts is None:
+          runopts = dict()
+        unittest.TestSuite.__init__(self)
+        self._dt= dt
+        self.dt_opts, self.dt_checker, self.dt_ropts = \
+                        optionflags, checker, runopts
+        for idx in xrange(len(dt.examples)):
+            unittest.TestSuite.addTest(self, \
+                                       self.docTestCaseClass(dt, idx))
+    
+    def addTest(self, test):
+        raise RuntimeError, "No test can be added to this Test Suite."
+    
+    def run(self, result):
+        self.docRunnerClass(optionflags= self.dt_opts, 
+                checker=self.dt_checker, verbose=False, \
+                result= result).run(self._dt, **self.dt_ropts)
+        return result
+    
+    def debug(self):
+        r"""Run the tests without collecting errors in a TestResult
+        """
+        self.run(None)
+        return result
+
+class DocTestLoader(unittest.TestLoader):
+    r"""This class loads DocTestCases and returns them wrapped in a
+    TestSuite
+    """
+    doctestSuiteClass= DocTestSuite
+    def __init__(self, dt_finder= None, globs=None, extraglobs=None, 
+                 **opts):
+        super(DocTestLoader, self).__init__()
+        self._dtf= dt_finder or doctest.DocTestFinder()
+        self.globs, self.extraglobs, self.opts= globs, extraglobs, opts
+    
+    def loadTestsFromTestCase(self, testCaseClass):
+        r"""Return a suite of all DocTestsCases contained in 
+        testCaseClass
+        """
+        raise NotImplementedError
+    
+    def loadTestsFromModule(self, module):
+        r"""Return a suite of all DocTestCases contained in the given 
+        module
+        """
+        return self.loadTestsFromObject(module)
+    
+    def loadModuleFromName(self, name):
+        parts_copy = name.split('.')
+        while parts_copy:
+            try:
+                module = __import__('.'.join(parts_copy))
+                return module
+            except ImportError:
+                del parts_copy[-1]
+                if not parts_copy: raise
+    
+    def findObjectByName(self, name, module=None):
+        parts = name.split('.')
+        if module is None:
+            module= self.loadModuleFromName(name)
+        parts = parts[1:]
+        obj = module
+        for part in parts:
+            parent, obj = obj, getattr(obj, part)
+        return obj
+        
+    def loadTestsFromObject(self, obj, module=None):
+        global modules
+        doctests = self._dtf.find(obj, module=module, 
+                                  globs=self.globs, 
+                                  extraglobs=self.extraglobs)
+        if module is None:
+            if ismodule(obj):
+                module= obj
+            else:
+                try:
+                    module= modules[obj.__module__]
+                except:
+                    module= None
+        if self.globs is None:
+            globs = module and module.__dict__ or dict()
+        
+        # This is legacy code inspired in doctest behavior.
+        # However this is not done anymore since it difficults loading
+        # tests from multiple test scripts by using PackageTestLoader
+        # class.
+            # if not doctests:
+            # # Why do we want to do this? Because it reveals a bug that
+            # # might otherwise be hidden.
+            #     raise ValueError(obj, "has no tests")
+        doctests.sort()
+        try:
+            filename = module and module.__file__ or '?'
+        except:
+            filename = '?'
+        if filename[-4:] in (".pyc", ".pyo"):
+            filename = filename[:-1]
+        ts= self.suiteClass()
+        for dt in doctests:
+            if len(dt.examples) != 0:
+                if not dt.filename:
+                    dt.filename = filename
+                ts.addTest(self.doctestSuiteClass(dt, **self.opts))
+        return ts
+    
+    def loadTestsFromName(self, name, module=None):
+        r"""Return a suite of all tests cases given a string specifier.
+        
+        The name may resolve to any kind of object.
+        
+        The method optionally resolves the names relative to a given 
+        module.
+        """
+        return self.loadTestsFromObject(
+                                 self.findObjectByName(name, module),
+                                 module)
+
+#------------------------------------------------------
+#	Default settings
+#------------------------------------------------------
+defaultTestRunner = unittest.TextTestRunner()
+
+#------------------------------------------------------
+#    Custom Test Loaders
+#------------------------------------------------------
+
+class MultiTestLoader(unittest.TestLoader):
+    r"""A loader which retrieves at once unittest-like test cases from
+    different sources and/or formats.
+    """
+    
+    def __init__(self, loaders=None):
+        self.loaders= loaders or []
+    def loadTestsFromTestCase(self, testCaseClass):
+        r"""Return a suite of all tests cases contained in 
+        testCaseClass
+        """
+        return self.suiteClass(
+                loader.loadTestsFromTestCase(testCaseClass) \
+                                        for loader in self.loaders)
+    def loadTestsFromModule(self, module):
+        r"""Return a suite of all tests cases contained in the given
+        module
+        """
+        return self.suiteClass(loader.loadTestsFromModule(module) \
+                for loader in self.loaders)
+    def loadTestsFromName(self, name, module=None):
+        r"""Return a suite of all tests cases given a string 
+        specifier.
+        """
+        return self.suiteClass(loader.loadTestsFromName(name, module) \
+                                        for loader in self.loaders)
+    def loadTestsFromNames(self, names, module=None):
+        r"""Return a suite of all tests cases found using the given 
+        sequence of string specifiers. See 'loadTestsFromName()'.
+        """
+        return self.suiteClass(loader.loadTestsFromNames(names, module) \
+                for loader in self.loaders)
+
+defaultTestLoader = MultiTestLoader([unittest.defaultTestLoader, 
+                                DocTestLoader()])
+
+class PackageTestLoader(unittest.TestLoader):
+    r"""A unittest-like loader (Decorator/Wrapper class) that recursively
+    retrieves all the tests included in all the modules found within a
+    specified package and the hierarchy it determines. Some filters
+    (i.e. regex) may be specified to limit the modules contributing to
+    the resulting test suite.
+    """
+    defaultPattern = re.compile(".*")
+    
+    def __init__(self, pattern=defaultPattern, loader=defaultTestLoader,
+                 impall=False, globs=None, ns=None):
+        r"""Initializes this test loader. Parameters have the following
+        meaning :
+        
+        param pattern: A regular expression object (see re module)
+            used to filter the modules which will be inspected so as
+            to retrieve the test suite. If not specified then all
+            modules will be processed looking for tests.
+            
+        param loader: Specify the loader used to retrieve test cases
+            from each (single) module matching the aforementioned
+            criteria.
+        
+        param impall: If the value of this flag evaluates to true then
+            all the modules inside the package hierarchy will be
+            imported (disregarding whether they will contribute to the
+            test suite or not). Otherwise only those packages for
+            which a match is made (i.e. those contributing to the test
+            suite) are imported directly.
+        
+        param globs: The global namespace in which module imports will
+            be carried out.
+        
+        param ns: The local namespace in which module imports will
+            be carried out.
+        """
+        if globs is None:
+          globs = dict()
+        if ns is None:
+          ns = dict()
+        super(PackageTestLoader, self).__init__()
+        self.pattern = pattern
+        self.loader = loader
+        self.impall = impall
+        self.locals = ns
+        self.globs = globs
+    
+    def loadTestsFromTestCase(self, testCaseClass):
+        r"""Return a suite of all tests cases contained in 
+        testCaseClass as determined by the wrapped test loader.
+        """
+        return self.loader.loadTestsFromTestCase(testCaseClass)
+        
+    def loadTestsFromModule(self, module):
+        r"""Return a suite of all test cases contained in the given
+        package and the modules it contains recursively.
+        
+        If a ``pattern`` was not specified to the initializer then all
+        modules (packages) in the aformentioned hierarchy are
+        considered. Otherwise a test suite is retrieved for all those
+        modules having a name matching this pattern. They are packed
+        together into another test suite (its type being standard
+        self.suiteClass) which is returned to the caller.
+        
+        If the attribute ``impall`` is set, then all modules in the
+        former hierarchy are imported disregarding whether they will
+        be inspected when looking for tests or not.
+        """
+        if os.path.basename(module.__file__) != '__init__.pyc':
+            return self.loader.loadTestsFromModule(module)
+        else:
+            loader = self.loader
+            root_dir = os.path.dirname(module.__file__)
+            pkg_name = module.__name__
+            idx = len(pkg_name)
+            pend_names = [module.__name__]
+            suite = self.suiteClass()
+            if self.pattern.match(module.__name__) is not None:
+                suite.addTest(loader.loadTestsFromModule(module))
+            for modnm in pend_names:
+                curdir = root_dir + modnm[idx:].replace('.', os.sep)
+                for fname in os.listdir(curdir):
+                    ch_path = os.path.join(curdir, fname)
+                    if os.path.isdir(ch_path) \
+                            and os.path.exists(os.path.join(ch_path, 
+                                               '__init__.py')):
+                        child_name = '.'.join([modnm, fname])
+                        pend_names.append(child_name)
+                    elif fname.endswith('.py') and \
+                            fname != '__init__.py':
+                        child_name = '.'.join([modnm, fname[:-3]])
+                    else:
+                        continue
+                    if self.pattern.match(child_name) is not None:
+                        __import__(child_name, self.globs,
+                                   self.locals, [], -1)
+                        suite.addTest(loader.loadTestsFromModule(
+                                modules[child_name]))
+                    elif self.impall:
+                        __import__(child_name, self.globs,
+                                   self.locals, [], -1)
+            return suite
+
+#------------------------------------------------------
+#	Patch to "fix" verbosity "bug" in unittest.TestProgram
+#------------------------------------------------------
+class VerboseTestProgram(unittest.TestProgram):
+    r"""A command-line program that runs a set of tests. 
+    This is primarily for making test modules conveniently executable.
+    
+    This class extends unittest.TestProgram for the following 
+    purposes:
+    
+      * Fix a minor bug in unittest.TestProgram which prevents running
+        from the command line a test suite using different verbosity
+        levels.
+        
+      * By default, load test cases from unittest.TestCase descendants
+        (i.e. by using unittest.TestLoader) as well as from
+        interactive examples included in doctests.
+    """
+    def __init__(self, module='__main__', defaultTest=None,
+                 argv=None, testRunner=None, 
+                 testLoader=defaultTestLoader):
+        return super(VerboseTestProgram, self).__init__(self, module, 
+                defaultTest, argv, testRunner, testLoader)
+    
+	def runTests(self):
+		if self.testRunner is not None:
+			self.testRunner.verbosity = self.verbosity
+		super(VerboseTestProgram, self).runTests()
+
+main = VerboseTestProgram