"""Experimental code for cleaner support of yap_ipython syntax with unittest.

In yap_ipython up until 0.10, we've used very hacked up nose machinery for running
tests with yap_ipython special syntax, and this has proved to be extremely slow.
This module provides decorators to try a different approach, stemming from a
conversation Brian and I (FP) had about this problem Sept/09.

The goal is to be able to easily write simple functions that can be seen by
unittest as tests, and ultimately for these to support doctests with full
yap_ipython syntax.  Nose already offers this based on naming conventions and our
hackish plugins, but we are seeking to move away from nose dependencies if
possible.

This module follows a different approach, based on decorators.

- A decorator called @ipdoctest can mark any function as having a docstring
  that should be viewed as a doctest, but after syntax conversion.

Authors
-------

- Fernando Perez <Fernando.Perez@berkeley.edu>
"""


#-----------------------------------------------------------------------------
#  Copyright (C) 2009-2011  The yap_ipython Development Team
#
#  Distributed under the terms of the BSD License.  The full license is in
#  the file COPYING, distributed as part of this software.
#-----------------------------------------------------------------------------

#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------

# Stdlib
import re
import unittest
from doctest import DocTestFinder, DocTestRunner, TestResults

#-----------------------------------------------------------------------------
# Classes and functions
#-----------------------------------------------------------------------------

def count_failures(runner):
    """Count number of failures in a doctest runner.

    Code modeled after the summarize() method in doctest.
    """
    return [TestResults(f, t) for f, t in runner._name2ft.values() if f > 0 ]


class IPython2PythonConverter(object):
    """Convert yap_ipython 'syntax' to valid Python.

    Eventually this code may grow to be the full yap_ipython syntax conversion
    implementation, but for now it only does prompt conversion."""
    
    def __init__(self):
        self.rps1 = re.compile(r'In\ \[\d+\]: ')
        self.rps2 = re.compile(r'\ \ \ \.\.\.+: ')
        self.rout = re.compile(r'Out\[\d+\]: \s*?\n?')
        self.pyps1 = '>>> '
        self.pyps2 = '... '
        self.rpyps1 = re.compile ('(\s*%s)(.*)$' % self.pyps1)
        self.rpyps2 = re.compile ('(\s*%s)(.*)$' % self.pyps2)

    def __call__(self, ds):
        """Convert yap_ipython prompts to python ones in a string."""
        from . import globalipapp

        pyps1 = '>>> '
        pyps2 = '... '
        pyout = ''

        dnew = ds
        dnew = self.rps1.sub(pyps1, dnew)
        dnew = self.rps2.sub(pyps2, dnew)
        dnew = self.rout.sub(pyout, dnew)
        ip = globalipapp.get_ipython()

        # Convert input yap_ipython source into valid Python.
        out = []
        newline = out.append
        for line in dnew.splitlines():

            mps1 = self.rpyps1.match(line)
            if mps1 is not None:
                prompt, text = mps1.groups()
                newline(prompt+ip.prefilter(text, False))
                continue

            mps2 = self.rpyps2.match(line)
            if mps2 is not None:
                prompt, text = mps2.groups()
                newline(prompt+ip.prefilter(text, True))
                continue
            
            newline(line)
        newline('')  # ensure a closing newline, needed by doctest
        #print "PYSRC:", '\n'.join(out)  # dbg
        return '\n'.join(out)

    #return dnew


class Doc2UnitTester(object):
    """Class whose instances act as a decorator for docstring testing.

    In practice we're only likely to need one instance ever, made below (though
    no attempt is made at turning it into a singleton, there is no need for
    that).
    """
    def __init__(self, verbose=False):
        """New decorator.

        Parameters
        ----------

        verbose : boolean, optional (False)
          Passed to the doctest finder and runner to control verbosity.
        """
        self.verbose = verbose
        # We can reuse the same finder for all instances
        self.finder = DocTestFinder(verbose=verbose, recurse=False)

    def __call__(self, func):
        """Use as a decorator: doctest a function's docstring as a unittest.
        
        This version runs normal doctests, but the idea is to make it later run
        ipython syntax instead."""

        # Capture the enclosing instance with a different name, so the new
        # class below can see it without confusion regarding its own 'self'
        # that will point to the test instance at runtime
        d2u = self

        # Rewrite the function's docstring to have python syntax
        if func.__doc__ is not None:
            func.__doc__ = ip2py(func.__doc__)

        # Now, create a tester object that is a real unittest instance, so
        # normal unittest machinery (or Nose, or Trial) can find it.
        class Tester(unittest.TestCase):
            def test(self):
                # Make a new runner per function to be tested
                runner = DocTestRunner(verbose=d2u.verbose)
                map(runner.run, d2u.finder.find(func, func.__name__))
                failed = count_failures(runner)
                if failed:
                    # Since we only looked at a single function's docstring,
                    # failed should contain at most one item.  More than that
                    # is a case we can't handle and should error out on
                    if len(failed) > 1:
                        err = "Invalid number of test results:" % failed
                        raise ValueError(err)
                    # Report a normal failure.
                    self.fail('failed doctests: %s' % str(failed[0]))
                    
        # Rename it so test reports have the original signature.
        Tester.__name__ = func.__name__
        return Tester


def ipdocstring(func):
    """Change the function docstring via ip2py.
    """
    if func.__doc__ is not None:
        func.__doc__ = ip2py(func.__doc__)
    return func

        
# Make an instance of the classes for public use
ipdoctest = Doc2UnitTester()
ip2py = IPython2PythonConverter()