[PATCH 07/13] Add documentation to the simpleline library for TUI

Martin Sivak msivak at redhat.com
Wed Aug 8 08:52:45 UTC 2012


---
 pyanaconda/ui/tui/simpleline/base.py    |  294 +++++++++++++++++++++++++------
 pyanaconda/ui/tui/simpleline/widgets.py |   87 ++++++++-
 2 files changed, 323 insertions(+), 58 deletions(-)

diff --git a/pyanaconda/ui/tui/simpleline/base.py b/pyanaconda/ui/tui/simpleline/base.py
index 573f28b..0c9f750 100644
--- a/pyanaconda/ui/tui/simpleline/base.py
+++ b/pyanaconda/ui/tui/simpleline/base.py
@@ -19,19 +19,47 @@
 # Red Hat Author(s): Martin Sivak <msivak at redhat.com>
 #
 
-__all__ = ["ExitMainLoop", "App", "UIScreen", "Widget"]
+__all__ = ["App", "UIScreen", "Widget"]
 
 import readline
 
 class ExitAllMainLoops(Exception):
+    """This exceptions ends the whole App mainloop structure. App.run() quits
+       after it is processed."""
     pass
 
 class ExitMainLoop(Exception):
+    """This eceptions ends the outermost mainloop. Used internally when dialogs
+       close."""
     pass
 
 
 class App(object):
+    """This is the main class for TUI screen handling. It is responsible for
+       mainloop control and keeping track of the screen stack.
+
+       Screens are organized in stack structure so it is possible to return
+       to caller when dialog or sub-screen closes.
+
+       It supports four window transitions:
+       - show new screen replacing the current one (linear progression)
+       - show new screen keeping the current one in stack (hub & spoke)
+       - show new screen and wait for it to end (dialog)
+       - close current window and return to the next one in stack
+       """
+
     def __init__(self, title, yes_or_no_question = None, width = 80):
+        """
+        :param title: application title for whenewer we need to display app name
+        :type title: unicode
+
+        :param yes_or_no_question: UIScreen object class used for Quit dialog
+        :type yes_or_no_question: class UIScreen accepting additional message arg
+
+        :param width: screen width for rendering purposes
+        :type width: int
+        """
+
         self._header = title
         self._spacer = "\n".join(2*[width*"="])
         self._width = width
@@ -40,36 +68,90 @@ class App(object):
         # screen stack contains triplets
         #  UIScreen to show
         #  arguments for it's show method
-        #  value indicating whether new mainloop is needed - None = do nothing, True = execute, False = already running, exit when window closes
+        #  value indicating whether new mainloop is needed
+        #   - None = do nothing
+        #   - True = execute new loop
+        #   - False = already running loop, exit when window closes
         self._screens = []
 
     def switch_screen(self, ui, args = None):
-        """Schedules a screen to replace the current one."""
+        """Schedules a screen to replace the current one.
+
+        :param ui: screen to show
+        :type ui: instance of UIScreen
+
+        :param args: optional argument to pass to ui's refresh method (can be used to select what item should be displayed or so)
+        :type args: anything
+
+        """
+
         oldscr, oldattr, oldloop = self._screens.pop()
+
+        # we have to keep the oldloop value so we stop
+        # dialog's mainloop if it ever uses switch_screen
         self._screens.append((ui, args, oldloop))
         self.redraw()
 
     def switch_screen_with_return(self, ui, args = None):
-        """Schedules a screen to show, but keeps the current one in stack to return to, when the new one is closed."""
+        """Schedules a screen to show, but keeps the current one in stack
+           to return to, when the new one is closed.
+
+        :param ui: screen to show
+        :type ui: UIScreen instance
+
+        :param args: optional argument, please see switch_screen for details
+        :type args: anything
+        """
+
         self._screens.append((ui, args, None))
         self.redraw()
 
     def switch_screen_modal(self, ui, args = None):
-        """Starts a new screen right away, so the caller can collect data back. When the new screen is closed, the caller is redisplayed."""
+        """Starts a new screen right away, so the caller can collect data back.
+        When the new screen is closed, the caller is redisplayed.
+
+        This method does not return until the new screen is closed.
+
+        :param ui: screen to show
+        :type ui: UIScreen instance
+
+        :param args: optional argument, please see switch_screen for details
+        :type args: anything
+        """
+
+        # set the third item to True so new loop gets started
         self._screens.append((ui, args, True))
         self._do_redraw()
 
     def schedule_screen(self, ui, args = None):
-        """Add screen to the bottom of the stack."""
+        """Add screen to the bottom of the stack. This is mostly usefull
+        at the beginning to prepare the first screen hierarchy to display.
+
+        :param ui: screen to show
+        :type ui: UIScreen instance
+
+        :param args: optional argument, please see switch_screen for details
+        :type args: anything
+        """
         self._screens.insert(0, (ui, args, None))
 
     def close_screen(self, scr = None):
+        """Close the currently displayed screen and exit it's main loop
+        if necessary. Next screen from the stack is then displayed.
+
+        :param scr: if an UIScreen instance is passed it is checked to be the screen we are trying to close.
+        :type scr: UIScreen instance
+        """
+
         oldscr, oldattr, oldloop = self._screens.pop()
         if scr is not None:
             assert oldscr == scr
 
+        # this cannot happen, if we are closing the window,
+        # the loop must have been running or not be there at all
+        assert oldloop != True
+
         # we are in modal window, end it's loop
-        assert oldloop != True # this cannot happen, if we are closing the window, the loop must be running or not there
         if oldloop == False:
             raise ExitMainLoop()
 
@@ -80,19 +162,32 @@ class App(object):
 
     def _do_redraw(self):
         """Draws the current screen and returns True if user input is requested.
-           If modal screen is requested, starts a new loop and initiates redraw after it ends."""
+           If modal screen is requested, starts a new loop and initiates redraw after it ends.
+
+           :return: this method returns True if user input processing is requested
+           :rtype: bool
+           """
+
+        # there is nothing to display, exit
         if not self._screens:
             raise ExitMainLoop()
 
+        # get the screen from the top of the stack
         screen, args, newloop = self._screens[-1]
 
+        # new mainloop is requested
         if newloop == True:
+            # change the record to indicate mainloop is running
             self._screens.pop()
             self._screens.append((screen, args, False))
+            # start the mainloop
             self.mainloop()
+            # after the mainloop ends, set the redraw flag
+            # and skip the input processing once, to redisplay the screen first
             self.redraw()
-            input_needed = False # we have to skip input once, to redisplay the screen first
+            input_needed = False
         else:
+            # get the widget tree from the screen and show it in the screen
             input_needed = screen.refresh(args)
             screen.window.show_all()
             self._redraw = False
@@ -100,57 +195,99 @@ class App(object):
         return input_needed
 
     def run(self):
+        """This methods starts the application. Do not use self.mainloop() directly
+        as run() handles all the required exceptions needed to keep nested mainloops
+        working."""
+
         try:
             self.mainloop()
         except ExitAllMainLoops:
             pass
 
     def mainloop(self):
+        """Single mainloop. Do not use directly, start the application using run()."""
+
+        # ask for redraw by default
         self._redraw = True
+
+        # inital state
         last_screen = None
         error_counter = 0
+
+        # run until there is nothing else to display
         while self._screens:
+            # if redraw is needed, separate the content on the screen from the
+            # stuff we are about to display now
             if self._redraw:
                 print self._spacer
 
             try:
+                # draw the screen if redraw is needed or the screen changed
+                # (unlikely to happen separately, but just be sure)
                 if self._redraw or last_screen != self._screens[-1]:
+                    # we have fresh screen, reset error counter
+                    error_counter = 0
                     if not self._do_redraw():
+                        # if no input processing is requested, go for another cycle
                         continue
 
                 last_screen = self._screens[-1][0]
 
+                # get the screen's prompt
                 prompt = last_screen.prompt()
+
+                # None means prompt handled the input by itself
+                # ask for redraw and continue
                 if prompt is None:
                     self.redraw()
                     continue
 
+                # get the input from user
                 c = raw_input(prompt)
 
+                # process the input, if it wasn't processed (valid)
+                # increment the error counter
                 if not self.input(c):
                     error_counter += 1
 
+                # redraw the screen after 5 bad inputs
                 if error_counter >= 5:
                     self.redraw()
 
-                if self._redraw:
-                    error_counter = 0
+            # end just this loop
             except ExitMainLoop:
                 break
+
+            # propagate higher to end all loops
+            # not really needed here, but we might need
+            # more processing in the future
             except ExitAllMainLoops:
                 raise
 
     def input(self, key):
-        """Method called to process unhandled input key presses."""
+        """Method called internally to process unhandled input key presses.
+        Also handles the main quit and close commands.
+
+        :param key: the string entered by user
+        :type key: unicode
+
+        :return: True if key was processed, False if it was not recognized
+        :rtype: True|False
+
+        """
+
+        # delegate the handling to active screen first
         if self._screens:
             key = self._screens[-1][0].input(key)
             if key is None:
                 return True
 
+        # global close command
         if self._screens and (key == 'c'):
             self.close_screen()
             return True
 
+        # global quit command
         elif self._screens and (key == 'q'):
             if self.quit_question:
                 d = self.quit_question(self, u"Do you really want to quit?")
@@ -162,6 +299,7 @@ class App(object):
         return False
 
     def redraw(self):
+        """Set the redraw flag so the screen is refreshed as soon as possible."""
         self._redraw = True
 
     @property
@@ -169,29 +307,51 @@ class App(object):
         return self._header
 
     @property
-    def store(self):
-        return self._store
-
-    @property
     def width(self):
+        """Return the total width of screen space we have available."""
         return self._width
 
 class UIScreen(object):
+    """Base class representing one TUI Screen. Shares some API with anaconda's GUI
+    to make it easy for devs to create similar UI with the familiar API."""
+
+    # title line of the screen
     title = u"Screen.."
 
     def __init__(self, app):
+        """
+        :param app: reference to application main class
+        :type app: instance of class App
+        """
+
         self._app = app
+
+        # list that holds the content to be printed out
         self._window = []
 
     def refresh(self, args = None):
-        """Method which prepares the screen to self._window. If user input is requested, return True."""
+        """Method which prepares the content desired on the screen to self._window.
+
+        :param args: optional argument passed from switch_screen calls
+        :type args: anything
+
+        :return: has to return True if input processing is requested, otherwise
+                 the screen will get printed and the main loop will continue
+        :rtype: True|False
+        """
+
         self._window = [self.title, u""]
+        return True
 
     @property
     def window(self):
+        """Return reference to the window instance. In TUI, just return self."""
         return self
 
     def show_all(self):
+        """Prepares all elements of self._window for output and then prints
+        them on the screen."""
+
         for w in self._window:
             if hasattr(w, "render"):
                 w.render(self.app.width)
@@ -200,32 +360,49 @@ class UIScreen(object):
     show = show_all
 
     def hide(self):
+        """This does nothing in TUI, it is here to make API similar."""
         pass
 
     def input(self, key):
-        """Method called to process input. If the input is not handled here, return it."""
+        """Method called to process input. If the input is not handled here, return it.
+
+        :param key: input string to process
+        :type key: unicode
+
+        :return: return True or None if key was handled, False if the screen should not
+                 process input on the App and key if you want it to.
+        :rtype: True|False|None|unicode
+        """
+
         return key
 
     def prompt(self):
-        """Return the text to be shown as prompt or handle the prompt and return None."""
+        """Return the text to be shown as prompt or handle the prompt and return None.
+
+        :return: returns text to be shown next to the prompt for input or None
+                 to skip further input processing
+        :rtype: unicode|None
+"""
         return u"\tPlease make your choice from above ['q' to quit]: "
 
     @property
     def app(self):
+        """The reference to this Screen's assigned App instance."""
         return self._app
 
     def close(self):
+        """Close the current screen."""
         self.app.close_screen(self)
 
 class Widget(object):
     def __init__(self, max_width = None, default = None):
         """Initializes base Widgets buffer.
 
-           @param max_width server as a hint about screen size to write method with default arguments
-           @type max_width int
+           :param max_width: server as a hint about screen size to write method with default arguments
+           :type max_width: int
 
-           @param default string containing the default content to fill the buffer with
-           @type default string
+           :param default: string containing the default content to fill the buffer with
+           :type default: string
            """
 
         self._buffer = []
@@ -236,10 +413,13 @@ class Widget(object):
 
     @property
     def height(self):
+        """The current height of the internal buffer."""
         return len(self._buffer)
 
     @property
     def width(self):
+        """The current width of the internal buffer
+           (id of the first empty column)."""
         return reduce(lambda acc,l: max(acc, len(l)), self._buffer, 0)
 
     def clear(self):
@@ -255,8 +435,8 @@ class Widget(object):
     def render(self, width = None):
         """This method has to redraw the widget's self._buffer.
 
-           @param width the width of buffer requested by the caller
-           @type width int
+           :param width: the width of buffer requested by the caller
+           :type width: int
 
            This method will commonly call render of child widgets and then draw and write
            methods to copy their contents to self._buffer
@@ -264,10 +444,18 @@ class Widget(object):
         self.clear()
 
     def __unicode__(self):
+        """Method to render the screen when printing as unicode string."""
         return u"\n".join([u"".join(l) for l in self._buffer])
 
     def setxy(self, row, col):
-        """Sets cursor position."""
+        """Sets cursor position.
+
+        :param row: row id, starts with 0 at the top of the screen
+        :type row: int
+
+        :param col: column id, starts with 0 on the left side of the screen
+        :type col: int
+        """
         self._cursor = (row, col)
 
     @property
@@ -281,32 +469,33 @@ class Widget(object):
     def draw(self, w, row = None, col = None, block = False):
         """This method copies w widget's content to this widget's buffer at row, col position.
 
-           @param w widget to take content from
-           @type w class Widget
+           :param w: widget to take content from
+           :type w: class Widget
 
-           @param row row number to start at (default is at the cursor position)
-           @type row int
+           :param row: row number to start at (default is at the cursor position)
+           :type row: int
 
-           @param col column number to start at (default is at the cursor position)
-           @type col int
+           :param col: column number to start at (default is at the cursor position)
+           :type col: int
 
-           @param block when printing newline, start at column col (True) or at column 0 (False)
-           @type boolean
+           :param block: when printing newline, start at column col (True) or at column 0 (False)
+           :type block: boolean
            """
 
-
+        # if the starting row is not present, start at the cursor position
         if row is None:
             row = self._cursor[0]
 
+        # if the starting column is not present, start at the cursor position
         if col is None:
             col = self._cursor[1]
 
-        # fill up rows
+        # fill up rows to accomodate for w.height
         if self.height < row + w.height:
             for i in range(row + w.height - self.height):
                 self._buffer.append(list())
 
-        # append columns
+        # append columns to accomodate for w.width
         for l in range(row, row + w.height):
             l_len = len(self._buffer[l])
             w_len = len(w.content[l - row])
@@ -314,6 +503,7 @@ class Widget(object):
                 self._buffer[l] += ((col + w_len - l_len) * list(u" "))
             self._buffer[l][col:col + w_len] = w.content[l - row][:]
 
+        # move the cursor to new spot
         if block:
             self._cursor = (row + w.height, col)
         else:
@@ -322,20 +512,20 @@ class Widget(object):
     def write(self, text, row = None, col = None, width = None, block = False):
         """This method emulates typing machine writing to this widget's buffer.
 
-           @param text text to type
-           @type text unicode
+           :param text: text to type
+           :type text: unicode
 
-           @param row row number to start at (default is at the cursor position)
-           @type row int
+           :param row: row number to start at (default is at the cursor position)
+           :type row: int
 
-           @param col column number to start at (default is at the cursor position)
-           @type col int
+           :param col: column number to start at (default is at the cursor position)
+           :type col: int
 
-           @param width wrap at "col" + "width" column (default is at self._max_width)
-           @type width int
+           :param width: wrap at "col" + "width" column (default is at self._max_width)
+           :type width: int
 
-           @param block when printing newline, start at column col (True) or at column 0 (False)
-           @type boolean
+           :param block: when printing newline, start at column col (True) or at column 0 (False)
+           :type block: boolean
            """
 
         if row is None:
@@ -384,12 +574,12 @@ class Widget(object):
 
         self._cursor = (x, y)
 
-class HelloWorld(UIScreen):
-    def show(self, args = None):
-        print """Hello World\nquit by typing 'quit'"""
-        return True
-
 if __name__ == "__main__":
+    class HelloWorld(UIScreen):
+        def show(self, args = None):
+            print """Hello World\nquit by typing 'quit'"""
+            return True
+
     a = App("Hello World")
     s = HelloWorld(a, None)
     a.schedule_screen(s)
diff --git a/pyanaconda/ui/tui/simpleline/widgets.py b/pyanaconda/ui/tui/simpleline/widgets.py
index 8dde043..d6ef5c6 100644
--- a/pyanaconda/ui/tui/simpleline/widgets.py
+++ b/pyanaconda/ui/tui/simpleline/widgets.py
@@ -26,20 +26,49 @@ __all__ = ["TextWidget", "ColumnWidget", "CheckboxWidget", "CenterWidget"]
 import base
 
 class TextWidget(base.Widget):
+    """Class to handle wrapped text output."""
+
     def __init__(self, text):
+        """
+        :param text: text to format
+        :type text: unicode
+        """
+
         base.Widget.__init__(self)
         self._text = text
 
     def render(self, width):
+        """Renders the text widget limited to width number of columns
+        (wraps to the next line when the text is longer).
+
+        :param width: maximum width allocated to the string
+        :type width: int
+
+        :raises
+        """
+
         base.Widget.render(self, width)
         self.write(self._text, width = width)
 
 class CenterWidget(base.Widget):
+    """Class to handle horizontal centering of content."""
+
     def __init__(self, w):
+        """
+        :param w: widget to center
+        :type w: base.Widget
+        """
         base.Widget.__init__(self)
         self._w = w
 
     def render(self, width):
+        """
+        Render the centered widget to internal buffer.
+
+        :param width: maximum width the widget should use
+        :type width: int
+        """
+
         base.Widget.render(self, width)
         self._w.render(width)
         self.draw(self._w, col = (width - self._w.width) / 2)
@@ -48,11 +77,11 @@ class ColumnWidget(base.Widget):
     def __init__(self, columns, spacing = 0):
         """Create text columns
 
-           @param columns list containing (column width, [list of widgets to put into this column])
-           @type columns [(int, [...]), ...]
+           :param columns: list containing (column width, [list of widgets to put into this column])
+           :type columns: [(int, [...]), ...]
 
-           @param spacing number of spaces to use between columns
-           @type int
+           :param spacing: number of spaces to use between columns
+           :type spacing: int
            """
 
         base.Widget.__init__(self)
@@ -60,26 +89,60 @@ class ColumnWidget(base.Widget):
         self._columns = columns
 
     def render(self, width):
+        """Render the widget to it's internal buffer
+
+        :param width: the maximum width the widget can use
+        :type width: int
+
+        :return: nothing
+        :rtype: None
+        """
+
         base.Widget.render(self, width)
 
+        # the lefmost empty column
         x = 0
+
+        # iterate over tuples (column width, column content)
         for col_width,col in self._columns:
+
+            # set cursor to first line and leftmost empty column
             self.setxy(0, x)
 
+            # if requested width is None, limit the maximum to width
+            # and set minimum to 0
             if col_width is None:
                 col_max_width = width - self.cursor[1]
                 col_width = 0
             else:
                 col_max_width = col_width
 
+            # render and draw contents of column
             for item in col:
                 item.render(col_max_width)
                 self.draw(item, block = True)
 
+            # recompute the leftmost empty column
             x = max((x + col_width), self.width) + self._spacing
 
 class CheckboxWidget(base.Widget):
-    def __init__(self, key = None, title = None, text = None, completed = None):
+    """Widget to show checkbox with (un)checked box, name and description."""
+
+    def __init__(self, key = "x", title = None, text = None, completed = None):
+        """
+        :param key: tick character to be used inside [ ]
+        :type key: character
+
+        :param title: the title next to the [ ] box
+        :type title: unicode
+
+        :param text: the description text to be shown on the second row in ()
+        :type text: unicode
+
+        :param completed: is the checkbox ticked or not?
+        :type completed: True|False
+        """
+
         base.Widget.__init__(self)
         self._key = key
         self._title = title
@@ -87,37 +150,49 @@ class CheckboxWidget(base.Widget):
         self._completed = completed
 
     def render(self, width):
+        """Render the widget to internal buffer. It should be max width
+           characters wide."""
         base.Widget.render(self, width)
 
         if self.completed:
-            checkchar = "x"
+            checkchar = self._key
         else:
             checkchar = " "
 
+        # prepare the checkbox
         checkbox = TextWidget("[%s]" % checkchar)
 
         data = []
 
+        # append lines
         if self.title:
             data.append(TextWidget(self.title))
 
         if self.text:
             data.append(TextWidget("(%s)" % self.text))
 
+        # the checkbox has two columns
+        # [x] is one and is 3 chars wide
+        # text is second and can occupy width - 3 - 1 (for space) chars
         cols = ColumnWidget([(3, [checkbox]), (width - 4, data)], 1)
         cols.render(width)
+
+        # transfer the column widget rendered stuff to internal buffer
         self.draw(cols)
 
     @property
     def title(self):
+        """Returns the first line (main title) of the checkbox."""
         return self._title
 
     @property
     def completed(self):
+        """Returns the state of the checkbox, checked is True."""
         return self._completed
 
     @property
     def text(self):
+        """Contains the description text from the second line."""
         return self._text
 
 if __name__ == "__main__":
-- 
1.7.10.4



More information about the anaconda-patches mailing list