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

Vratislav Podzimek vpodzime at redhat.com
Wed Aug 8 11:11:33 UTC 2012


Any reason for the different syntax of the docstrings?

On Wed, 2012-08-08 at 10:52 +0200, Martin Sivak wrote:
> ---
>  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__":

-- 
Vratislav Podzimek

Anaconda Rider | Red Hat, Inc. | Brno - Czech Republic



More information about the anaconda-patches mailing list