4

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.

anon01
  • 10,618
  • 8
  • 35
  • 58

2 Answers2

3

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).

Elias Dorneles
  • 22,556
  • 11
  • 85
  • 107
  • Uh, now that I think of it, probably `play_or_exit` should be a method of App, and you could pass `unhandled_input=app.play_or_exit)` :) – Elias Dorneles Mar 12 '18 at 23:07
  • So, I tried something like this initially: wrapped a widget in an object with an `update_widget` method that would change `app.widget` with a keystroke. `app.widget` did indeed change, but the loop never reflected that. Any thoughts? – anon01 Mar 13 '18 at 04:33
  • Right, you're not supposed to replace the main app widget. You can update the contents of the Pile widget (say, replacing the urwid.Text widget for other widgets), but you shouldn't replace the main app widget. What are you trying to do? – Elias Dorneles Mar 13 '18 at 09:02
  • My interface has a few pages with very different layouts (different widgets grouped into a Pile/Frame etc). I'd like to switch between them via keystroke. – anon01 Mar 13 '18 at 16:25
  • 1
    Right, so you should update the contents of the Pile/Frame widgets through the `.contents` attribute. See the docs for [Pile.contents](http://urwid.org/reference/widget.html#urwid.Pile.contents) and [Frame.contents](http://urwid.org/reference/widget.html#urwid.Frame.contents) – Elias Dorneles Mar 13 '18 at 21:26
  • This is helpful elias. I've found it's much easier to implement callback functionality common in the tutorial as wrapped object/method calls. Is there any reason this wouldn't be advisable (I'm thinking, a race condition under the hood or something)? – anon01 Mar 14 '18 at 00:03
  • @ConfusinglyCuriousTheThird not a problem at all, I'd say that using objects and method callbacks that's probably the way to go! Tbh, I wouldn't rely too much on the examples from the tutorial, those are kind of showing some age. Do what fits best your design. :) – Elias Dorneles Mar 14 '18 at 09:56
  • @EliasDorneles can you elaborate how to make the custom widgets respond to the keyboard events? Implementing the `keypress` fro children doesn't work apparently, only the parent is notifies, i.e., the event doesn't flow downstream. – cYrus Dec 05 '20 at 20:17
  • @EliasDorneles basically this unanswered question: https://stackoverflow.com/q/65018591/477168 – cYrus Dec 05 '20 at 20:27
2

When python compiles a function, left-hand-side variables that are the target of assignment are treated as local and the rest are global. loop is not assigned, so when python runs loop.widget = urwid.Filler(...), it knows that loop is not a local variable and it will look the name up in the module's namespace.

Module namespaces are dynamic, so as long as loop = urwid.MainLoop(app_state.main_widget, unhandled_input=callback) runs before the lookup, loop is created and it works. Since the callback can't be executed until loop.run(), loop will be defined.

This is one of the classic risks of singletons and global state. Its not always easy to make sure the resource is created before it is used.

tdelaney
  • 73,364
  • 6
  • 83
  • 116
  • thank you for the answer. Can you propose a better alternative? This is the best way I've found to dynamically modify `loop.widget`, but it seems pretty ugly. – anon01 Mar 12 '18 at 18:10
  • As a minimum, add a comment that `loop` is used in the UI (maybe `# "loop" used in UI events"`) so a future programmer doesn't rename it. I don't know `urwin` but its common for UI callbacks to have references back to its component's objects. Otherwise, what you are doing is common, so carry on! – tdelaney Mar 12 '18 at 18:21