[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