Search code examples
pythonfunctional-programmingcircular-dependencyurwid

preferred method to dynamically change the urwid.MainLoop widget


I was looking over a bit of code rooted in urwid:

import urwid
from functools import partial
from random import randint

class State(object):

    def __init__(self, main_widget):
        self.main_widget = main_widget

def handle_keystroke(app_state, key):
        if key in ('q', 'Q'):
            raise urwid.ExitMainLoop()
        else:
            loop.widget = urwid.Filler(urwid.Button('new rand int:' + str(randint(0, 100))))

app_state = State(urwid.Filler(urwid.Button('original widget')))

callback = partial(handle_keystroke, app_state)

loop = urwid.MainLoop(app_state.main_widget, unhandled_input=callback)
loop.run()

and noticed that loop is referenced in the function unhandled_input before it's defined. Furthermore, it's not passed as a parameter, it's just hard coded into the function by name. 1) Why is this possible, and: 2) is there a clearer alternative? It is difficult to do otherwise, as there is a circular dependencies of loop, app_state and callback.


Solution

  • I'm not sure how much of your sample code represents the original code, but it looks like you may want to get familiar with the technique of using urwid's custom widgets wrapping text widgets, as shown in the answer with an example widget that displays a text content one line at the time.

    Here is an example of writing something similar to the sample code you provided, in a design that fits urwid and Python a bit better:

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    
    from __future__ import print_function, absolute_import, division
    import urwid
    from random import randint
    
    
    class RandomNumberWidget(urwid.WidgetWrap):
        def __init__(self):
            self.random_number = None
            self.text_widget = urwid.Text(u'')
            super(RandomNumberWidget, self).__init__(self.text_widget)
    
        def roll(self):
            self.random_number = randint(0, 100)
            self.update()
    
        def update(self):
            """Update UI
            """
            if self.random_number is None:
                self.text_widget.set_text('No number set')
            else:
                self.text_widget.set_text('Random number: %s' % self.random_number)
    
    
    class App(object):
        def __init__(self):
            self.random_number_widget = RandomNumberWidget()
            top_message = 'Press any key to get a random number, or q to quit\n\n\n'
            widget = urwid.Pile([
                urwid.Padding(urwid.Text(top_message),
                              'center', width=('relative', len(top_message))),
                self.random_number_widget,
            ])
            self.widget = urwid.Filler(widget, 'top')
    
        def play(self):
            self.random_number_widget.roll()
    
        def play_or_exit(self, key):
            if key in ('q', 'Q', 'esc'):
                raise urwid.ExitMainLoop()
            app.play()
    
    
    if __name__ == '__main__':
        app = App()
        loop = urwid.MainLoop(app.widget, unhandled_input=app.play_or_exit)
        loop.run()
    

    Depending also on what you actually want to do, it could make sense to make the custom widgets respond to the keyboard events, instead of doing it all in the global handler (which is totally fine for simple programs, IMO).