Source

vinstall / vinstall / core / application.py

#-*- coding: utf-8 -*-

"""Provides the Application class, in charge of managing the different
application states.

"""


__author__ = "rbistolfi"
__all__ = ["Application"]


from .log import get_logger
from .command import ProcessingFacade
from.view import disable_buttons
from inspect import isgeneratorfunction
from Queue import Queue
import core
import sys


LOG = get_logger(__name__)


class Application(object):
    """Small implementation of a state pattern for managing the application
    flow. Must be intialized with the controller class implementing the first
    stage of the application. This class takes the control from there, and
    guides the process until the final stage, collecting the data provided by
    the user and executing the commands provided by the controller objects
    in the final stage.

    """
    def __init__(self, view, first_stage, config=None):
        """Application instance initialization.
        Arguments are a view, and the first controller class.

        """
        self.config = config or {}
        self.view = view
        self.import_view_module()
        self.current_render = None

        #run first stage... first
        self.current_stage = first_stage()
        self.current_stage.config = self.config

        #commands
        self.commands = []

        LOG.info("Application started")
        LOG.debug("Using view: %s" % view)

    def set_callbacks(self):
        """Set the next and previous controller as callbacks of the window's
        next and previous buttons.

        """
        main_window = self.current_render.main_window
        main_window.add_next_callback(self.next)
        main_window.add_previous_callback(self.previous)
        LOG.debug("Callbacks set")

    def import_view_module(self):
        """Get the view module and import it into the global namespace.

        """
        mod_name = "vinstall.ui"

        try:
            module = __import__(mod_name, globals(), locals(), [self.view],
                    level=1)
        except ImportError:
            #2.7 behaves differently
            module = __import__(mod_name, globals(), locals(), [self.view],
                    level=0)

        sys.modules[mod_name] = module
        LOG.debug("Imported view module %s: %s" % (self.view, module))

    def show(self):
        """Present the current state of the application to the user.

        """
        stage = self.current_stage
        LOG.debug("Loaded controller: %s" % stage)
        self.current_render = stage.render()
        self.current_render.main_window.clear_callbacks()
        self.current_render.update_main_window()
        self.set_callbacks()

    def next(self, *args):
        """Jump to the next step of the application, processing the current
        state first.

        """
        process_method = getattr(self.current_stage, "process", None)
        if process_method:
            self.handle_process()
        command_method = getattr(self.current_stage, "command", None)
        if command_method:
            self.handle_command()

        #next
        next_stage = self.current_stage.next()
        LOG.debug("Next controller class: %s" % next_stage)
        #check for the end of the app
        if next_stage is None:
            self.process_queue()
            return

        #run the next stage, run its init method if defined before rendering
        self.current_stage = next_stage()
        self.current_stage.config = self.config
        LOG.debug("Next controller set: %s" % self.current_stage)
        init_method = getattr(self.current_stage, "init", None)
        if init_method:
            self.handle_init()
        self.show()

    def previous(self, *args):
        """Jump to the previous state of the application.

        """
        previous = self.current_stage.previous()
        LOG.debug("Previous controller set to: %s" % previous)

        #check for the end of the app
        if previous is None:
            self.stop()

        self.current_stage = previous()
        self.current_stage.config = self.config
        previous_command = getattr(self.current_stage, "command", None)
        if previous_command:
            self.commands.pop()
            LOG.debug("Removed command %s from the command queue" %
                    previous_command)
        init_method = getattr(self.current_stage, "init", None)
        if init_method:
            self.handle_init()
        LOG.debug("Next controller set to: %s" % self.current_stage)
        self.show()

    def handle_init(self):
        """If the controller has an init method, run it first. We will decorate
        it with the disable_buttons decorator for avoiding user input during
        controller initialization.

        """
        init = disable_buttons(self.current_stage.init)
        LOG.debug("Found init method, initializing controller")
        init()

    def handle_command(self):
        """Push the command into the processing queue
        We use the temporary list in self.commands so we can handle "previous" properly.

        """
        command = self.current_stage.command
        LOG.debug("Command found: %s" % command)
        args = self.current_render.get_user_input()
        LOG.debug("Adding command: %s, %s" % (command, args))
        self.commands.append((command, args))

    def handle_process(self):
        """Handle the process method of the current controller

        """
        process = self.current_stage.process
        LOG.debug("Processing function found: %s" % process)
        values = self.current_render.get_user_input()
        LOG.debug("Processing values %s" % values)
        apply(process, values)
        LOG.debug("Data processed.")

    def process_queue(self):
        """Build a processing object and process the commands queue.
        There are two cases to handle. If the function provided by the user
        is a generator function, we initialize it using the user input as
        arguments and we use the result of yield to build a command object,
        pushing it to the processing queue. If it is a regular function,
        we push a tuple of (func, args).

        """
        LOG.debug("Building processing queue")
        processing = ProcessingFacade(Queue())
        for item in self.commands:
            func, args = item
            if isgeneratorfunction(func):
                LOG.debug("Command function %s is a generator function" % func)
                for f, a, desc in func(*args):
                    LOG.debug("Adding %s to the queue, with args: %s" % (f, a))
                    processing.add_command(f, args=a, description=desc)
            else:
                LOG.debug("Adding function %s to the queue, with args: %s"
                          % (func, args))
                processing.add_command(func, args=args, description=func.__doc__)

        LOG.debug("Processing commands")
        render = type(self.current_render)
        self.current_render = render("Processing the requested tasks",
                                     "Please wait until we execute the required"
                                     " operations.",
                                     processing)
        main_window = self.current_render.main_window
        main_window.clear_callbacks()
        main_window.add_next_callback(lambda *_: self.stop())
        self.current_render.update_main_window()
        processing.execute_all()

    def run(self):
        """Run the application. This method will import the view module first,
        and start the UI loop.

        """
        LOG.debug("Starting main loop")
        self.show()
        self.current_render.main_window.run()

    def stop(self):
        """Stop the application.

        """
        LOG.debug("Stopping main loop")
        self.current_render.main_window.stop()
        sys.exit(0)