"""DocTest wxPython and other python programs from a GUI interface.

Normally invoked from the command line as:

guitest [path]

This shows and a TreeCtrl with all of the reachable python files.
(Reachable means in the specified directory or a package therein).
Clicking on a file will doctest that package, displaying the
output to a TextCtrl with some basic colorization. Clicking on a
package will test all python files in the package.

This has some features specifically designed to make it play nice with
wxPython. First, the normal doctest loop has been turned inside out.
The wxPython event loop is in control and executes one  test on each
pass through OnIdle.

In addition, if a single file is being tested, the tester sniffs for
wx.Yield() [or wxYield()] in the tests and pauses on each occurence.
This allows the user to examine the tested widget. The sniffing
criteria and the delay are set by DocTester.should_pause and
DoctesterGui.delay_us respectively.

>>> tester = DoctesterGui(None, -1, '.')
>>> x = tester.Show(True)

Now we'd like to run the tester on guitest itself, but if we aren't
careful we'll get an infinite recursion. To avoid that we remove
__doc__ from guitest temporarily.

>>> import guitest; doc = guitest.__doc__; del guitest.__doc__

We run the tests on all other docstrings and check that the log comes
out right.

>>> tester.StartTests('./guitest.py')
>>> x = wx.wxYield()
>>> tester.log
['Testing .\\\\guitest.py...', 'OK', ' (3 tests)\\n', 'Done']

>>> guitest.__doc__ = doc
>>> x = tester.Destroy()


Missing Features:
  * Setabble delay
  * Change root
  * Reloadable files and packages
  * <esc> should end pauses.

"""
from __future__ import generators
import doctest
import os
import re
import sys
from wxPython import wx
from wxgui import tools


#----------------------------------------------------------------
# The non-GUI portion of the tester.
#----------------------------------------------------------------


def default_should_pause(example):
    return example[0].endswith("wxYield()") or example[0].endswith("wx.Yield()")

class DocTester:
    """Records which tests *would* have been run by doctest

    Normal usage is:

    examples = DocTestRecorder.recordmod(...)    

    Note: because I replace doctest._run_examples_inner in recordmod,
    this could potentially do horrible things if another thread was
    trying to use doctest.

    """

    PAUSE = "PAUSE"
    CONTINUE = "CONTINUE"
    
    def __init__(self, should_pause=default_should_pause):
        self.examples = []
        self.realrei = None
        self.should_pause = default_should_pause
        
    def __call__(self, out, fakeout, examples, globs, verbose, name, compileflags):
        self.examples.append((out, fakeout, examples, globs.copy(), verbose, name, compileflags))
        return 0, 0

    def clear(self):
        del self.examples[:]

    def install(self):
        self.realrei = doctest._run_examples_inner
        doctest._run_examples_inner = self

    def recordmod(self, m, name=None, globs=None, verbose=None, isprivate=None):
        """"Analogous to doctest.testmod, but returns a list of examples that would have been run"""
        self.clear()
        if not doctest._ismodule(m):
            raise TypeError("testmod: module required; " + `m`)
        if name is None:
            name = m.__name__
        try:
            self.install()
            tester = doctest.Tester(m, globs=globs, verbose=verbose, isprivate=isprivate)
            tester.rundoc(m, name)
            tester.rundict(m.__dict__, name, m)
            if hasattr(m, "__test__"):
                testdict = m.__test__
                if testdict:
                    if not hasattr(testdict, "items"):
                        raise TypeError("testmod: module.__test__ must support "
                                        ".items(); " + `testdict`)
                    tester.run__test__(testdict, name + ".__test__")
        finally:
            self.uninstall()
        return self.examples

    def testmod(self, module):
        examples = self.recordmod(module, verbose=False)
        fails, trys = 0, 0
        output = []
        for out, fakeout, examples, globs, verbose, name, compileflags in examples:
            out = output.append
            saveout = sys.stdout
            for ex in examples:
                sys.stdout = fakeout
                try:
                    f, t = doctest._run_examples_inner(out, fakeout, [ex], globs, verbose, name, compileflags)
                    fails += f
                    trys += t
                finally:
                    sys.stdout = saveout
                yield {True : self.PAUSE, False : self.CONTINUE}[self.should_pause(ex)]
        yield output, fails, trys 

    def uninstall(self):
        assert self.realrei is not None, "never installed"
        doctest._run_examples_inner = self.realrei
        self.realrei = None

doctester = DocTester()


def get_paths(origin='.'):
    """Return paths to python files from origin descending into packages.    
    """
    paths = [os.path.split(origin)[1]]
    if os.path.isdir(origin):
        for path in os.listdir(origin):
            path = os.path.abspath(os.path.join(origin, path))
            name = os.path.basename(path)
            if os.path.isfile(path):
                if os.path.splitext(name)[1] == '.py':
                    paths.append(name)
            elif os.path.isdir(path):
                if "__init__.py" in [os.path.split(x)[1] for x in os.listdir(path)]:
                    paths.append(get_paths(path))
        if '__init__.py' in paths:
            paths.remove('__init__.py')
            paths.insert(1, '__init__.py')
        return paths
    else:
        return paths[0]
    

def get_leaves(tree, origin=None, sep=os.sep):
    """Return the leaves of a tree defined as a set of nested lists/tuples.

    The first item in each sequence is the name of that branch. The full paths
    from the root are returned as the leaves.

    >>> tree = ['root',
    ...         ['branch1','leaf1', 'leaf2'],
    ...         'leaf1',
    ...         ('branch2', ['branch3', 'leaf1', 'leaf2'], 'leaf1')]
    >>> get_leaves(tree, sep='.')
    ['root.branch1.leaf1', 'root.branch1.leaf2', 'root.leaf1', 'root.branch2.branch3.leaf1', 'root.branch2.branch3.leaf2', 'root.branch2.leaf1']

    If an origin is specified, it is tacked onto the beginning of each leaf.

    >>> get_leaves(tree, 'real_root', sep='.')[:2]    
    ['real_root.root.branch1.leaf1', 'real_root.root.branch1.leaf2']

    """
    if isinstance(tree, (list, tuple)):
        leaves = []
        name = tree[0]
        if origin is None:
            origin = name
        else:
            origin = sep.join([origin, name])
        for branch in tree[1:]:
            leaves += get_leaves(branch, origin, sep)
        return leaves
    else:
        if origin is None:
            return [tree]
        else:
            return [sep.join([origin, tree])]


def import_name(name):
    try:
        mod = __import__(name)
        components = name.split('.')
        for comp in components[1:]:
            mod = getattr(mod, comp)
        return mod
    except:
        raise ImportError('could not import %s' % name)


#----------------------------------------------------------------
# The GUI portion of the tester.
#----------------------------------------------------------------

ID_Timer  = wx.wxNewId()

class DoctesterGui(wx.wxFrame):
    
    IMPORT_FAILURE = "IMPORT_FAILURE"
    STOPPED, INTEST, RUNNING = range(3)
    delay_ms = 3000
    
    def __init__(self, parent, id, path='.'):        
        wx.wxFrame.__init__(self, parent, id, "guitest")
        self.sizer = wx.wxBoxSizer(wx.wxHORIZONTAL)
        
        self.tree = DocTreeCtrl(self, -1, path)
        self.sizer.Add(self.tree, 1, wx.wxEXPAND)

        self.output = wx.wxTextCtrl(self, -1, style=wx.wxTE_MULTILINE|wx.wxTE_RICH2|wx.wxTE_DONTWRAP)
        self.output.SetEditable(False)
        self.sizer.Add(self.output, 2, wx.wxEXPAND)

        self.timer = wx.wxTimer(self, ID_Timer)

        tools.SetupSizer(self, self.sizer)
        self.SetSize((600,400))

        self.state = self.STOPPED
        self.log = []

        wx.EVT_CLOSE(self, self.OnClose)
        wx.EVT_IDLE(self, self.OnIdle)
        wx.EVT_TIMER(self, ID_Timer, self.OnTimer)
        
    def OnClose(self, event):
        self.Destroy()
        raise SystemExit()

    def OnTimer(self, event):
        self.state = DocTester.CONTINUE
        wx.wxWakeUpIdle()

    def OnIdle(self, event):
        if self.state != self.STOPPED:
            leaves, testiter = self.testdata
            if leaves:
                try:
                    path = leaves[0]
                    if self.state == self.RUNNING:
                        self.Write("Testing %s..." % path)
                    result = testiter.next()
                    if result == DocTester.PAUSE:
                        self.state = self.STOPPED
                        self.timer.Start(self.delay_ms, True)
                        return
                    elif result == DocTester.CONTINUE:
                        self.state = self.INTEST                        
                    elif result == self.IMPORT_FAILURE:
                        self.state = self.RUNNING
                        self.Write(" IMPORT FAILED\n", wx.wxRED)
                    else:
                        self.state = self.RUNNING
                        output, fails, trys = result
                        if trys:
                            if fails == 0:
                                self.Write("OK", wx.wxGREEN)
                            else:
                                self.Write("\n")
                                self.Write("".join(output))
                                self.Write("%s failures" % fails, wx.wxRED)
                            self.Write(" (%s tests)\n" % trys)
                        else:
                            self.Write(" NO TESTS\n", wx.wxBLUE)
                        del leaves[0]
                    event.RequestMore()
                except:
                    self.state = self.STOPPED
                    raise
            else:
                self.state = self.STOPPED
                self.Write("Done")
                wx.wxEnableTopLevelWindows(True)
                self.SetCursor(wx.wxSTANDARD_CURSOR)

    def RunTests(self, leaves):
        doctest.master = None
        for path in leaves[:]:
            name = path[:-3].replace(os.sep, '.')
            while name[0] == '.':
                name = name[1:]
            try:
                module = import_name(name)
            except:
                yield self.IMPORT_FAILURE
                continue
            for result in doctester.testmod(module):
                if len(leaves) > 1 and result == DocTester.PAUSE:
                    result = DocTester.CONTINUE
                yield result        
    
    def StartTests(self, path):
        self.output.Clear()
        leaves = get_leaves(get_paths(path), os.path.dirname(path))
        testiter = self.RunTests(leaves)
        self.testdata = leaves, testiter
        self.state = self.RUNNING
        wx.wxEnableTopLevelWindows(False)
        self.SetCursor(wx.wxHOURGLASS_CURSOR)
        wx.wxSafeYield()
        wx.wxWakeUpIdle()

    def Write(self, text, color=wx.wxBLACK):
        start = self.output.GetLastPosition()
        self.output.AppendText(text)
        self.log .append(text)
        self.output.SetStyle(start, self.output.GetLastPosition(), wx.wxTextAttr(color))
        wx.wxSafeYield()
        

class DocTreeCtrl(wx.wxTreeCtrl):
    
    def __init__(self, parent, id, path):
        wx.wxTreeCtrl.__init__(self, parent, id, style=wx.wxTR_HAS_BUTTONS)

        isz = (16,16)
        il = wx.wxImageList(isz[0], isz[1])
        self.fldridx     = il.Add(wx.wxArtProvider_GetBitmap(wx.wxART_FOLDER,      wx.wxART_OTHER, isz))
        self.fldropenidx = il.Add(wx.wxArtProvider_GetBitmap(wx.wxART_FILE_OPEN,   wx.wxART_OTHER, isz)) 
        self.fileidx     = il.Add(wx.wxArtProvider_GetBitmap(wx.wxART_REPORT_VIEW, wx.wxART_OTHER, isz))

        self.SetImageList(il)
        self.il = il

        paths = get_paths(path)
        
        self.root = self.AddRoot(path)
        self.AddChildren(self.root, paths[1:])        

        self.SetItemImage(self.root, self.fldridx, wx.wxTreeItemIcon_Normal)
        self.SetItemImage(self.root, self.fldropenidx, wx.wxTreeItemIcon_Expanded)

        self.Expand(self.root)

        wx.EVT_LEFT_DCLICK(self, self.OnLeftDClick)
        wx.EVT_RIGHT_DOWN(self, self.OnRightClick)


    def AddChildren(self, parent, paths):
        for p in paths:
            if isinstance(p, (list, tuple)):
                child = self.AppendItem(parent, p[0])
                self.SetItemImage(child, self.fldridx, wx.wxTreeItemIcon_Normal)
                self.SetItemImage(child, self.fldropenidx, wx.wxTreeItemIcon_Expanded)
                self.AddChildren(child, p[1:])
            else:
                child = self.AppendItem(parent,  p)
                self.SetItemImage(child, self.fileidx, wx.wxTreeItemIcon_Normal)
                self.SetItemImage(child, self.fileidx, wx.wxTreeItemIcon_Selected)

    def GetPath(self, item):
        comps = []
        current = item
        while True:
            text = self.GetItemText(current)
            if not text:
                break
            comps.append(text)
            current = self.GetItemParent(current)
        comps.reverse()
        return os.path.join(*comps)

    def OnRightClick(self, event):
        # XXX Eventually add popup menu with Test/Reload options.
        item, flags = self.HitTest(event.GetPosition())
        self.SelectItem(item)

    def OnLeftDClick(self, event):
        item, flags = self.HitTest(event.GetPosition())
        path = self.GetPath(item)
        self.GetParent().StartTests(path)


        

if __name__ == "__main__":
    assert len(sys.argv) in (1,2), "%s takes at most one argument" % sys.argv[0]
    if len(sys.argv) == 2:
        path = sys.argv[1]
    else:
        path = '.'
    app = wx.wxPySimpleApp()
    tgui = DoctesterGui(None, -1, path)
    tgui.Show(True)
    app.MainLoop()
 
    

