A Portable Program-Launch Framework

With all of these different ways to start programs on different platforms, it can be difficult to remember what tools to use in a given situation. Moreover, some of these tools are called in ways that are complicated and thus easy to forget (for me, at least). I write scripts that need to launch Python programs often enough that I eventually wrote a module to try to hide most of the underlying details. While I was at it, I made this module smart enough to automatically pick a launch scheme based on the underlying platform. Laziness is the mother of many a useful module.

Example 5-25 collects in a single module many of the techniques we’ve met in this chapter. It implements an abstract superclass, LaunchMode, which defines what it means to start a Python program, but it doesn’t define how. Instead, its subclasses provide a run method that actually starts a Python program according to a given scheme, and (optionally) define an announce method to display a program’s name at startup time.

Example 5-25. PP3Elaunchmodes.py

###############################################################
# launch Python programs with reusable launcher scheme classes;
# assumes 'python' is on your system path (but see Launcher.py)
###############################################################

import sys, os
pyfile = (sys.platform[:3] == 'win' and 'python.exe') or 'python'

def findPythonExe( ):
    try:                                                # get path to python
        pypath = sys.executable                         # use sys in newer pys
    except AttributeError:                              # else env or search
        try:
            pypath = os.environ['PP3E_PYTHON_FILE']     # run by launcher?
        except KeyError:                                # if so configs env
            from Launcher import which, guessLocation
            pypath = (which(pyfile, trace=False) or
                      guessLocation(pyfile, trace=False))
    return pypath

class LaunchMode:
    def _ _init_ _(self, label, command):
        self.what  = label
        self.where = command
    def _ _call_ _(self):                   # on call, ex: button press callback
        self.announce(self.what)
        self.run(self.where)                # subclasses must define run( )
    def announce(self, text):               # subclasses may redefine announce( )
        print text                          # methods instead of if/elif logic
    def run(self, cmdline):
        assert 0, 'run must be defined'

class System(LaunchMode):                          # run shell commands
    def run(self, cmdline):                        # caveat: blocks caller
        pypath = findPythonExe( )
        os.system('%s %s' % (pypath, cmdline))     # unless '&' added on Linux

class Popen(LaunchMode):                           # caveat: blocks caller
    def run(self, cmdline):                        # since pipe closed too soon
        pypath = findPythonExe( )
        os.popen(pypath + ' ' + cmdline)

class Fork(LaunchMode):
    def run(self, cmdline):
        assert hasattr(os, 'fork')                  # for Unix systems today
        cmdline = cmdline.split( )                  # convert string to list
        if os.fork( ) == 0:                         # start new child process
            pypath = findPythonExe( )
            os.execvp(pypath, [pyfile] + cmdline)  # run new program in child

class Start(LaunchMode):
    def run(self, cmdline):                        # for Windows only
        assert sys.platform[:3] == 'win'           # runs independent of caller
        os.startfile(cmdline)                      # uses Windows associations

class StartArgs(LaunchMode):
    def run(self, cmdline):                        # for Windows only
        assert sys.platform[:3] == 'win'           # args may require real start
        os.system('start ' + cmdline)              # creates pop-up window

class Spawn(LaunchMode):                           # for Windows or Unix
    def run(self, cmdline):                        # run python in new process
        pypath = findPythonExe( )                       # runs independent of caller
        os.spawnv(os.P_DETACH, pypath, (pyfile, cmdline)) # P_NOWAIT: dos box

class Top_level(LaunchMode):
    def run(self, cmdline):                           # new window, same process
        assert 0, 'Sorry - mode not yet implemented'  # tbd: need GUI class info

if sys.platform[:3] == 'win':
    PortableLauncher = Spawn            # pick best launcher for platform
else:                                   # need to tweak this code elsewhere
    PortableLauncher = Fork

class QuietPortableLauncher(PortableLauncher):
    def announce(self, text):
        pass

def selftest( ):
    myfile  = 'launchmodes.py'
    program = 'Gui/TextEditor/textEditor.py ' + myfile        # assume in cwd
    raw_input('default mode...')
    launcher = PortableLauncher('PyEdit', program)
    launcher( )                                                # no block

    raw_input('system mode...')
    System('PyEdit', program)( )                               # blocks

    raw_input('popen mode...')
    Popen('PyEdit', program)( )                                # blocks

    if sys.platform[:3] == 'win':
        raw_input('DOS start mode...')                        # no block
        StartArgs('PyEdit', os.path.normpath(program))( )

if _ _name_ _ == '_ _main_ _': selftest( )

Near the end of the file, the module picks a default class based on the sys.platform attribute: PortableLauncher is set to a class that uses spawnv on Windows and one that uses the fork/exec combination elsewhere (in recent Pythons, we could probably just use the spawnv scheme on most platforms, but the alternatives in this module are used in additional contexts). If you import this module and always use its PortableLauncher attribute, you can forget many of the platform-specific details enumerated in this chapter.

To run a Python program, simply import the PortableLauncher class, make an instance by passing a label and command line (without a leading “python” word), and then call the instance object as though it were a function. The program is started by a call operation instead of a method so that the classes in this module can be used to generate callback handlers in Tkinter-based GUIs. As we’ll see in the upcoming chapters, button-presses in Tkinter invoke a callable object with no arguments; by registering a PortableLauncher instance to handle the press event, we can automatically start a new program from another program’s GUI.

When run standalone, this module’s selftest function is invoked as usual. On both Windows and Linux, all classes tested start a new Python text editor program (the upcoming PyEdit GUI program again) running independently with its own window. Figure 5-2 shows one in action on Windows; all spawned editors open the launchmodes.py source file automatically, because its name is passed to PyEdit as a command-line argument. As coded, both System and Popen block the caller until the editor exits, but PortableLauncher (really, Spawn or Fork) and Start do not:[*]

PyEdit program spawned from launchmodes

Figure 5-2. PyEdit program spawned from launchmodes

C:...PP3E>python launchmodes.py
default mode...
PyEdit
system mode...
PyEdit
popen mode...
PyEdit
DOS start mode...
PyEdit

As a more practical application, this file is also used by launcher scripts designed to run examples in this book in a portable fashion. The PyDemos and PyGadgets scripts at the top of this book’s examples distribution directory tree (described in the Preface) simply import PortableLauncher and register instances to respond to GUI events. Because of that, these two launcher GUIs run on both Windows and Linux unchanged (Tkinter’s portability helps too, of course). The PyGadgets script even customizes PortableLauncher to update a label in a GUI at start time.

class Launcher(launchmodes.PortableLauncher):    # use wrapped launcher class
    def announce(self, text):                    # customize to set GUI label
        Info.config(text=text)

We’ll explore these scripts in Part III (but feel free to peek at the end of Chapter 10 now). Because of this role, the Spawn class in this file uses additional tools to search for the Python executable’s path, which is required by os.spawnv. If the sys.executable path string is not available in an older version of Python that you happen to be using, it calls two functions exported by a file named Launcher.py to find a suitable Python executable regardless of whether the user has added its directory to his system PATH variable’s setting. The idea is to start Python programs, even if Python hasn’t been installed in the shell variables on the local machine. Because we’re going to meet Launcher.py in the next chapter, though, I’m going to postpone further details for now.



[*] This is fairly subtle. Technically, Popen blocks its caller only because the input pipe to the spawned program is closed too early, when the os.popen call’s result is garbage collected in Popen.run; os.popen normally does not block (in fact, assigning its result here to a global variable postpones blocking, but only until the next Popen object run frees the prior result). On Linux, adding an & to the end of the constructed command line in the System and Popen.run methods makes these objects no longer block their callers when run. Since the fork/exec, spawnv, and system/start schemes seem at least as good in practice, these Popen block states have not been addressed. Note too that the StartArgs scheme may not generate a DOS console pop-up window in the self-test if the text editor program file’s name ends in a .pyw extension; starting .py program files normally creates the console pop-up box.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.101.81