5
\$\begingroup\$

TL;DR I have an answer, that seems to work, but I'm not happy that it's a good answer.

I am exploring some algorithms that are easiest to debug graphically and piecemeal, so I'm using a python-Tk GUI to interact with them. I know there are other graphical options out there for python, but for all its faults, Tk is the one I know, and have written in.

I've got to the point now where the 'right' thing to do is to have recursive function calls. I would like the option to see what each further level of call does, or several levels, or abort the whole drill-down if I see it's on the wrong track. Logging/printing is great, up to a point, but I really need the graphics as well.

What I would like to do is have a 'wait on user input' call in the recursive function, then I can display state, and follow its decent level by level.

Unfortunately, when user code runs, it blocks the Tk GUI, so a simple 'single step' button on the display doesn't work when the user code is busy. I've used threads in Tk, but only for web and cache fetches, Tk gets confused if any but the main thread draws to the GUI. At the moment all of my output is drawn directly, and revamping the code to draw from queues would be a mountain to climb.

While I could recast the recursive calls as single calls, and manually handle the state between calls, which would allow user code to exit and Tk take control again, that is what I was doing. I realised that to do this, I was coding myself more and more spaghetti, when what I really needed was a simple recursive function, and to let python handle the state and call logic.

My first attempt at a solution had the wait function polling for the existence of a file, let the OS handle the concurrency! It worked, but needed a separate program to handle the file, and felt very clunky. I'm sure communicating through a socket would work as well, in a similar way, with the same issues. I should learn how to use sockets, but not today.

After much searching, I finally came across widget.wait_window(target). If the target is a TopLevel window, it does stay live, even though its parent process is blocked by waiting user code.

Below is my attempt to use that idea in as tidy or minimal way as possible. I wanted a module, and minimal intrusion into the function being debugged. It works, but I seem to have got a lot of cruft round it, which suggests there may be better ways of doing it.

Obviously the factorial function in my if name==main app is a toy example of a recursive function to exercise the pauser.

""" module pauser - provide tk-compatible single/multi-step inspection pause."""

import tkinter as tki

class Pauser():
    def __init__(self, parent, xoff=300, yoff=100):

        """ create a Pauser object

        Args:
            parent:      A visible Tk object, used to get initial position for the pauser window
            xoff, yoff:  Optional offsets to specify relative initial position
        """

        self.to_do = 0
        self.last_rep = 1
        self.parent = parent

        # Tk has this bizarre feature that telling and asking screen coords use different origins
        # let's self-cal the offsets here, just in case they change in the future, or for style
        self.top = tki.Toplevel()
        self.top.geometry('+200+200') # it's onscreen on any conceivable system
        self.top.update()
        self.xoffset = 200 - self.top.winfo_rootx()
        self.yoffset = 200 - self.top.winfo_rooty()
        self.top.destroy()

        # set initial position of pauser window relative to parent
        self.tx = self.parent.winfo_rootx()+xoff
        self.ty = self.parent.winfo_rooty()+yoff        

    def _step(self, *args):
        self.ret_value = 'paused'
        try:
            reps = int(self.nbox.get())
        except ValueError:
            reps = 1
        self.last_rep = reps
        self.to_do = reps-1      
        self._and_return()

    def _abort(self, *args):
        self.ret_value = 'abort'
        self._and_return()

    def _and_return(self):
        # save where it is now, user might have moved the window        
        self.tx = self.top.winfo_rootx()
        self.ty = self.top.winfo_rooty()        
        self.top.destroy()        

    def __call__(self):
        """When called, wait for user response

        Bind Enter key to GO, Escape key to abort, and all other keys to Entry

        Return values:
            'paused':   return waited for user action
            'no pause': return was fast, due to a multi-step being set earlier
            'abort':    user hit the abort key or Escape
        """        

        if self.to_do>0:
            self.to_do -= 1
            return 'no pause'

        self.top = tki.Toplevel()
        self.top.title('[]')  # there's not room for one with all the controls
        self.top.geometry('+{}+{}'.format(self.tx+self.xoffset, self.ty+self.yoffset))
        self.top.update()

        self.nbox = tki.Entry(self.top, width=10)
        self.nbox.grid(row=2, column=0)
        self.nbox.insert(0,'{}'.format(self.last_rep))

        self.go_but = tki.Button(self.top, text='STEP', command=self._step)
        self.go_but.grid(row=2, column=1)

        self.buttonabort = tki.Button(self.top, text="abort", command=self._abort)
        self.buttonabort.grid(row=3, columnspan=2)

        # and setup the window to receive keystrokes        
        self.top.grab_set()
        self.top.bind("<Return>", self._step)
        self.top.bind("<Escape>", self._abort)
        self.nbox.selection_range(0, tki.END)  # so we can just overtype the number
        self.nbox.focus_set()

        self.parent.wait_window(self.top)

        return self.ret_value



if __name__=='__main__':

    class AbortException(Exception):
        pass    

    class App(tki.Frame):
        def __init__(self, parent):
            tki.Frame.__init__(self)
            self.parent = parent

            self.do_stuff = tki.Button(self, text='evaluate factorial', command=self.start)
            self.do_stuff.grid()

            self.data = tki.Entry(self, width=5)
            self.data.grid()
            self.data.insert(0, '6')

            self.debug_var = tki.IntVar()
            self.debug_check = tki.Checkbutton(self, text='debug', variable=self.debug_var)
            self.debug_check.grid()
            self.debug_var.set(1)

            self.debug_out = tki.Label(self)
            self.debug_out.grid()

            self.quit_button = tki.Button(self, text='quit', command=self.quit)
            self.quit_button.grid()

        def quit(self):
            quit()

        def start(self):
            number = int(self.data.get())
            print('going to compute factorial ', number)

            self.pauser = Pauser(self, 200, -200)
            self.debugging = self.debug_var.get()

            try:
                fact = self.factorial(number)
                print('result ', fact)
            except AbortException:
                print('aborted that run')        

        def factorial(self, n):

            if self.debugging:
                print('entering factorial with argument ', n)
                self.debug_out.config(text='factorial arg = {}'.format(n))
                self.update_idletasks()
                rval = self.pauser()
                print('rval was - ', rval)
                if rval=='abort':
                    raise AbortException()

            if n==1:
                return 1
            else:
                return n*self.factorial(n-1)



    root = tki.Tk()
    app = App(root)
    app.pack()
    app.mainloop()

Observations

My original attempt was a function, but initialisation seemed to take up a lot of 'try: except AttributeError:' clauses on function properties, when a class just has a simple init section.

Questions

1) Is this a reasonable way to do this? Is there any better way to do this?

2) I had a choice of the dunder call function, or a named function for the wait. Is either better or more pythonic?

3) I use an exception to burn out of any depth of stacked calls. I guess the alternative is returning a tuple of (result, abort_flag). Is there any strong reason to use one or the other. Use of the exception means I don't have to change the function signature when adding/removing the debug code.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.