pythonstackindentationcontextmanagerhierarchical

How to manage a stack of widgets in Python?


I'm working on a Python class that lets the caller add widgets to a custom GUI. To set up the GUI, the user would write a method that adds widgets (named or anonymous) to the widget area so that the widgets form a tree (as is common in GUIs).

In order to allow the user to set up a tree of widgets without having to give a name to every container widget (and then explicitly reference that parent widget every time a child widget is added), my API supports the notion of a "parent widget stack". When declaring a container widget, the user can specify to push that widget onto this stack, and then any further widgets (that don't explicitly specify a parent) will be added to the parent at the top of the stack by default. Here's a simple example of what I mean:

def SetupGUI(self):
   self.AddWidget(name="root", type="container", push=True)

   self.AddWidget(type="container", push=True)
   for i in range(0,8):
      self.AddWidget(name="button%i"%i, type="button")
   self.PopParentWidget()  # pop the buttons-container off the parents-stack

   self.AddWidget(type="container", push=True)
   for i in range(0,8):
      self.AddWidget(name="slider%i"%i, type="slider")
   self.PopParentWidget()  # pop the sliders-container off the parents-stack

   self.PopParentWidget()  # pop the container "root" off the parents-stack

This is convenient, but I find that when the GUI hierarchy gets more elaborate, it starts to become difficult to tell which call to self.PopParentWidget() corresponds to which container widget. It's easy to put in one too many, or one too few, and end up with highly amusing but unintended results in the GUI.

So my question is, short of forcing PopParentWidget() to take an explicit widget name (which I want to avoid, because I don't want to have to name every container widget), is there anything I can do to make the push/pop pairing in the code more apparent to the eye?

In C/C++ I would do it using indentation, but with Python I'm not allowed to do that. For example, I'd love to be able to do this:

def SetupGUI(self):
   self.AddWidget(name="root", type="container", push=True)
      self.AddWidget(type="container", push=True)
         for i in range(0,8):
            self.AddWidget(name="button%i"%i, type="button")
         self.PopParentWidget()  # pop the buttons-container off the parents-stack
      self.AddWidget(type="container", push=True)
         for i in range(0,8):
            self.AddWidget(name="slider%i"%i, type="slider")
         self.PopParentWidget()  # pop the sliders-container off the parents-stack
      self.PopParentWidget()  # pop the container "root" off the parents-stack

... but Python will throw up an IndentationError if I get creative like that.


Solution

  • This sort of situation -- where you have a pair of opposite operations -- calls for a context manager. Instead of explicitly pushing and popping container widgets onto/from a stack, you would wrap the children of the container in a with block. Building on top of the code you've shown here, this could be implemented as something like

    @contextlib.contextmanager
    def container(self, name=None):
        self.AddWidget(name=name, type='container', push=True)
        yield
        self.PopParentWidget()
    

    (Documentation for contextlib.contextmanager).

    Your SetupGUI method then becomes:

    def SetupGUI(self):
        with self.container(name='root'):
            with self.container():
                for i in range(0,8):
                    self.AddWidget(name='button%i' % i, type='button')
            with self.container():
                for i in range(0,8):
                    self.AddWidget(name='slider%i' % i, type='slider')
    

    As you can see, the nesting is clear from the indentation, and there's no need to manually push and pop.