git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@37014 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775
		
			
				
	
	
		
			526 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			526 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
#----------------------------------------------------------------------
 | 
						|
# Name:        wxversion
 | 
						|
# Purpose:     Allows a wxPython program to search for alternate 
 | 
						|
#              installations of the wxPython packages and modify sys.path
 | 
						|
#              so they will be found when "import wx" is done.
 | 
						|
#
 | 
						|
# Author:      Robin Dunn
 | 
						|
#
 | 
						|
# Created:     24-Sept-2004
 | 
						|
# RCS-ID:      $Id$
 | 
						|
# Copyright:   (c) 2004 by Total Control Software
 | 
						|
# Licence:     wxWindows license
 | 
						|
#----------------------------------------------------------------------
 | 
						|
 | 
						|
"""
 | 
						|
If you have more than one version of wxPython installed this module
 | 
						|
allows your application to choose which version of wxPython will be
 | 
						|
imported when it does 'import wx'.  The main function of this module
 | 
						|
is `select` and you use it like this::
 | 
						|
 | 
						|
    import wxversion
 | 
						|
    wxversion.select('2.4')
 | 
						|
    import wx
 | 
						|
 | 
						|
Or additional build options can also be selected, although they will
 | 
						|
not be required if they are not installed, like this::
 | 
						|
 | 
						|
    import wxversion
 | 
						|
    wxversion.select('2.5.3-unicode')
 | 
						|
    import wx
 | 
						|
 | 
						|
Or you can require an exact match on the build options like this::
 | 
						|
 | 
						|
    import wxversion
 | 
						|
    wxversion.select('2.5.3-unicode', optionsRequired=True)
 | 
						|
    import wx
 | 
						|
 | 
						|
Finally you can also specify a collection of versions that are allowed
 | 
						|
by your application, like this::
 | 
						|
 | 
						|
    import wxversion
 | 
						|
    wxversion.select(['2.5.4', '2.5.5', '2.6'])
 | 
						|
    import wx
 | 
						|
 | 
						|
 | 
						|
Of course the default wxPython version can also be controlled by
 | 
						|
setting PYTHONPATH or by editing the wx.pth path configuration file,
 | 
						|
but using wxversion will allow an application to manage the version
 | 
						|
selection itself rather than depend on the user to setup the
 | 
						|
environment correctly.
 | 
						|
 | 
						|
It works by searching the sys.path for directories matching wx-* and
 | 
						|
then comparing them to what was passed to the select function.  If a
 | 
						|
match is found then that path is inserted into sys.path.
 | 
						|
 | 
						|
NOTE: If you are making a 'bundle' of your application with a tool
 | 
						|
like py2exe then you should *not* use the wxversion module since it
 | 
						|
looks at the filesystem for the directories on sys.path, it will fail
 | 
						|
in a bundled environment.  Instead you should simply ensure that the
 | 
						|
version of wxPython that you want is found by default on the sys.path
 | 
						|
when making the bundled version by setting PYTHONPATH.  Then that
 | 
						|
version will be included in your bundle and your app will work as
 | 
						|
expected.  Py2exe and the others usually have a way to tell at runtime
 | 
						|
if they are running from a bundle or running raw, so you can check
 | 
						|
that and only use wxversion if needed.  For example, for py2exe::
 | 
						|
 | 
						|
    if not hasattr(sys, 'frozen'):
 | 
						|
        import wxversion
 | 
						|
        wxversion.select('2.5')
 | 
						|
    import wx
 | 
						|
 | 
						|
More documentation on wxversion and multi-version installs can be
 | 
						|
found at: http://wiki.wxpython.org/index.cgi/MultiVersionInstalls
 | 
						|
 | 
						|
"""
 | 
						|
 | 
						|
import re, sys, os, glob, fnmatch
 | 
						|
 | 
						|
 | 
						|
_selected = None
 | 
						|
class VersionError(Exception):
 | 
						|
    pass
 | 
						|
 | 
						|
#----------------------------------------------------------------------
 | 
						|
 | 
						|
def select(versions, optionsRequired=False):
 | 
						|
    """
 | 
						|
    Search for a wxPython installation that matches version.  If one
 | 
						|
    is found then sys.path is modified so that version will be
 | 
						|
    imported with a 'import wx', otherwise a VersionError exception is
 | 
						|
    raised.  This funciton should only be caled once at the begining
 | 
						|
    of the application before wxPython is imported.
 | 
						|
 | 
						|
        :param versions: Specifies the version to look for, it can
 | 
						|
            either be a string or a list of strings.  Each string is
 | 
						|
            compared to the installed wxPythons and the best match is
 | 
						|
            inserted into the sys.path, allowing an 'import wx' to
 | 
						|
            find that version.
 | 
						|
 | 
						|
            The version string is composed of the dotted version
 | 
						|
            number (at least 2 of the 4 components) optionally
 | 
						|
            followed by hyphen ('-') separated options (wx port,
 | 
						|
            unicode/ansi, flavour, etc.)  A match is determined by how
 | 
						|
            much of the installed version matches what is given in the
 | 
						|
            version parameter.  If the version number components don't
 | 
						|
            match then the score is zero, otherwise the score is
 | 
						|
            increased for every specified optional component that is
 | 
						|
            specified and that matches.
 | 
						|
 | 
						|
            Please note, however, that it is possible for a match to
 | 
						|
            be selected that doesn't exactly match the versions
 | 
						|
            requested.  The only component that is required to be
 | 
						|
            matched is the version number.  If you need to require a
 | 
						|
            match on the other components as well, then please use the
 | 
						|
            optional ``optionsRequired`` parameter described next.
 | 
						|
 | 
						|
        :param optionsRequired: Allows you to specify that the other
 | 
						|
             components of the version string (such as the port name
 | 
						|
             or character type) are also required to be present for an
 | 
						|
             installed version to be considered a match.  Using this
 | 
						|
             parameter allows you to change the selection from a soft,
 | 
						|
             as close as possible match to a hard, exact match.
 | 
						|
        
 | 
						|
    """
 | 
						|
    if type(versions) == str:
 | 
						|
        versions = [versions]
 | 
						|
 | 
						|
    global _selected
 | 
						|
    if _selected is not None:
 | 
						|
        # A version was previously selected, ensure that it matches
 | 
						|
        # this new request
 | 
						|
        for ver in versions:
 | 
						|
            if _selected.Score(_wxPackageInfo(ver), optionsRequired) > 0:
 | 
						|
                return
 | 
						|
        # otherwise, raise an exception
 | 
						|
        raise VersionError("A previously selected wx version does not match the new request.")
 | 
						|
 | 
						|
    # If we get here then this is the first time wxversion is used, 
 | 
						|
    # ensure that wxPython hasn't been imported yet.
 | 
						|
    if sys.modules.has_key('wx') or sys.modules.has_key('wxPython'):
 | 
						|
        raise VersionError("wxversion.select() must be called before wxPython is imported")
 | 
						|
    
 | 
						|
    # Look for a matching version and manipulate the sys.path as
 | 
						|
    # needed to allow it to be imported.
 | 
						|
    installed = _find_installed(True)
 | 
						|
    bestMatch = _get_best_match(installed, versions, optionsRequired)
 | 
						|
    
 | 
						|
    if bestMatch is None:
 | 
						|
        raise VersionError("Requested version of wxPython not found")
 | 
						|
 | 
						|
    sys.path.insert(0, bestMatch.pathname)
 | 
						|
    # q.v. Bug #1409256
 | 
						|
    path64 = re.sub('/lib/','/lib64/',bestMatch.pathname)
 | 
						|
    if os.path.isdir(path64):
 | 
						|
        sys.path.insert(0, path64)
 | 
						|
    _selected = bestMatch
 | 
						|
        
 | 
						|
#----------------------------------------------------------------------
 | 
						|
 | 
						|
UPDATE_URL = "http://wxPython.org/"
 | 
						|
#UPDATE_URL = "http://sourceforge.net/project/showfiles.php?group_id=10718"
 | 
						|
 | 
						|
_EM_DEBUG=0
 | 
						|
 | 
						|
def ensureMinimal(minVersion, optionsRequired=False):
 | 
						|
    """
 | 
						|
    Checks to see if the default version of wxPython is greater-than
 | 
						|
    or equal to `minVersion`.  If not then it will try to find an
 | 
						|
    installed version that is >= minVersion.  If none are available
 | 
						|
    then a message is displayed that will inform the user and will
 | 
						|
    offer to open their web browser to the wxPython downloads page,
 | 
						|
    and will then exit the application.
 | 
						|
    """
 | 
						|
    assert type(minVersion) == str
 | 
						|
 | 
						|
    # ensure that wxPython hasn't been imported yet.
 | 
						|
    if sys.modules.has_key('wx') or sys.modules.has_key('wxPython'):
 | 
						|
        raise VersionError("wxversion.ensureMinimal() must be called before wxPython is imported")
 | 
						|
 | 
						|
    bestMatch = None
 | 
						|
    minv = _wxPackageInfo(minVersion)
 | 
						|
 | 
						|
    # check the default version first
 | 
						|
    defaultPath = _find_default()
 | 
						|
    if defaultPath:
 | 
						|
        defv = _wxPackageInfo(defaultPath, True)
 | 
						|
        if defv >= minv and minv.CheckOptions(defv, optionsRequired):
 | 
						|
            bestMatch = defv
 | 
						|
 | 
						|
    # if still no match then check look at all installed versions
 | 
						|
    if bestMatch is None:
 | 
						|
        installed = _find_installed()
 | 
						|
        # The list is in reverse sorted order, so find the first
 | 
						|
        # one that is big enough and optionally matches the
 | 
						|
        # options
 | 
						|
        for inst in installed:
 | 
						|
            if inst >= minv and minv.CheckOptions(inst, optionsRequired):
 | 
						|
                bestMatch = inst
 | 
						|
                break
 | 
						|
 | 
						|
    # if still no match then prompt the user
 | 
						|
    if bestMatch is None:
 | 
						|
        if _EM_DEBUG: # We'll do it this way just for the test code below
 | 
						|
            raise VersionError("Requested version of wxPython not found")
 | 
						|
        
 | 
						|
        import wx, webbrowser
 | 
						|
        versions = "\n".join(["      "+ver for ver in getInstalled()])
 | 
						|
        app = wx.PySimpleApp()
 | 
						|
        result = wx.MessageBox("This application requires a version of wxPython "
 | 
						|
                               "greater than or equal to %s, but a matching version "
 | 
						|
                               "was not found.\n\n"
 | 
						|
                               "You currently have these version(s) installed:\n%s\n\n"
 | 
						|
                               "Would you like to download a new version of wxPython?\n"
 | 
						|
                               % (minVersion, versions),
 | 
						|
                      "wxPython Upgrade Needed", style=wx.YES_NO)
 | 
						|
        if result == wx.YES:
 | 
						|
            webbrowser.open(UPDATE_URL)
 | 
						|
        app.MainLoop()
 | 
						|
        sys.exit()
 | 
						|
 | 
						|
    sys.path.insert(0, bestMatch.pathname)
 | 
						|
    # q.v. Bug #1409256
 | 
						|
    path64 = re.sub('/lib/','/lib64/',bestMatch.pathname)
 | 
						|
    if os.path.isdir(path64):
 | 
						|
        sys.path.insert(0, path64)
 | 
						|
    global _selected
 | 
						|
    _selected = bestMatch
 | 
						|
        
 | 
						|
 | 
						|
#----------------------------------------------------------------------
 | 
						|
 | 
						|
def checkInstalled(versions, optionsRequired=False):
 | 
						|
    """
 | 
						|
    Check if there is a version of wxPython installed that matches one
 | 
						|
    of the versions given.  Returns True if so, False if not.  This
 | 
						|
    can be used to determine if calling `select` will succeed or not.
 | 
						|
 | 
						|
        :param versions: Same as in `select`, either a string or a list
 | 
						|
            of strings specifying the version(s) to check for.
 | 
						|
 | 
						|
        :param optionsRequired: Same as in `select`.
 | 
						|
    """
 | 
						|
    
 | 
						|
    if type(versions) == str:
 | 
						|
        versions = [versions]
 | 
						|
    installed = _find_installed()
 | 
						|
    bestMatch = _get_best_match(installed, versions, optionsRequired)
 | 
						|
    return bestMatch is not None
 | 
						|
 | 
						|
#----------------------------------------------------------------------
 | 
						|
 | 
						|
def getInstalled():
 | 
						|
    """
 | 
						|
    Returns a list of strings representing the installed wxPython
 | 
						|
    versions that are found on the system.
 | 
						|
    """
 | 
						|
    installed = _find_installed()
 | 
						|
    return [os.path.basename(p.pathname)[3:] for p in installed]
 | 
						|
 | 
						|
 | 
						|
 | 
						|
#----------------------------------------------------------------------
 | 
						|
# private helpers...
 | 
						|
 | 
						|
def _get_best_match(installed, versions, optionsRequired):
 | 
						|
    bestMatch = None
 | 
						|
    bestScore = 0
 | 
						|
    for pkg in installed:
 | 
						|
        for ver in versions:
 | 
						|
            score = pkg.Score(_wxPackageInfo(ver), optionsRequired)
 | 
						|
            if score > bestScore:
 | 
						|
                bestMatch = pkg
 | 
						|
                bestScore = score
 | 
						|
    return bestMatch
 | 
						|
 | 
						|
 | 
						|
_pattern = "wx-[0-9].*"
 | 
						|
def _find_installed(removeExisting=False):
 | 
						|
    installed = []
 | 
						|
    toRemove = []
 | 
						|
    for pth in sys.path:
 | 
						|
 | 
						|
        # empty means to look in the current dir
 | 
						|
        if not pth:
 | 
						|
            pth = '.'
 | 
						|
 | 
						|
        # skip it if it's not a package dir
 | 
						|
        if not os.path.isdir(pth):
 | 
						|
            continue
 | 
						|
        
 | 
						|
        base = os.path.basename(pth)
 | 
						|
 | 
						|
        # if it's a wx path that's already in the sys.path then mark
 | 
						|
        # it for removal and then skip it
 | 
						|
        if fnmatch.fnmatchcase(base, _pattern):
 | 
						|
            toRemove.append(pth)
 | 
						|
            continue
 | 
						|
 | 
						|
        # now look in the dir for matching subdirs
 | 
						|
        for name in glob.glob(os.path.join(pth, _pattern)):
 | 
						|
            # make sure it's a directory
 | 
						|
            if not os.path.isdir(name):
 | 
						|
                continue
 | 
						|
            # and has a wx subdir
 | 
						|
            if not os.path.exists(os.path.join(name, 'wx')):
 | 
						|
                continue
 | 
						|
            installed.append(_wxPackageInfo(name, True))
 | 
						|
 | 
						|
    if removeExisting:
 | 
						|
        for rem in toRemove:
 | 
						|
            del sys.path[sys.path.index(rem)]
 | 
						|
        
 | 
						|
    installed.sort()
 | 
						|
    installed.reverse()
 | 
						|
    return installed
 | 
						|
 | 
						|
 | 
						|
# Scan the sys.path looking for either a directory matching _pattern,
 | 
						|
# or a wx.pth file
 | 
						|
def _find_default():
 | 
						|
    for pth in sys.path:
 | 
						|
        # empty means to look in the current dir
 | 
						|
        if not pth:
 | 
						|
            pth = '.'
 | 
						|
 | 
						|
        # skip it if it's not a package dir
 | 
						|
        if not os.path.isdir(pth):
 | 
						|
            continue
 | 
						|
        
 | 
						|
        # does it match the pattern?
 | 
						|
        base = os.path.basename(pth)
 | 
						|
        if fnmatch.fnmatchcase(base, _pattern):
 | 
						|
            return pth
 | 
						|
 | 
						|
    for pth in sys.path:
 | 
						|
        if not pth:
 | 
						|
            pth = '.'
 | 
						|
        if not os.path.isdir(pth):
 | 
						|
            continue
 | 
						|
        if os.path.exists(os.path.join(pth, 'wx.pth')):
 | 
						|
            base = open(os.path.join(pth, 'wx.pth')).read()
 | 
						|
            return os.path.join(pth, base)
 | 
						|
 | 
						|
    return None
 | 
						|
 | 
						|
 | 
						|
class _wxPackageInfo(object):
 | 
						|
    def __init__(self, pathname, stripFirst=False):
 | 
						|
        self.pathname = pathname
 | 
						|
        base = os.path.basename(pathname)
 | 
						|
        segments = base.split('-')
 | 
						|
        if stripFirst:
 | 
						|
            segments = segments[1:]
 | 
						|
        self.version = tuple([int(x) for x in segments[0].split('.')])
 | 
						|
        self.options = segments[1:]
 | 
						|
 | 
						|
 | 
						|
    def Score(self, other, optionsRequired):
 | 
						|
        score = 0
 | 
						|
        
 | 
						|
        # whatever number of version components given in other must
 | 
						|
        # match exactly
 | 
						|
        minlen = min(len(self.version), len(other.version))
 | 
						|
        if self.version[:minlen] != other.version[:minlen]:
 | 
						|
            return 0        
 | 
						|
        score += 1
 | 
						|
 | 
						|
        # check for matching options, if optionsRequired then the
 | 
						|
        # options are not optional ;-)
 | 
						|
        for opt in other.options:
 | 
						|
            if opt in self.options:
 | 
						|
                score += 1
 | 
						|
            elif optionsRequired:
 | 
						|
                return 0
 | 
						|
            
 | 
						|
        return score
 | 
						|
 | 
						|
    
 | 
						|
    def CheckOptions(self, other, optionsRequired):
 | 
						|
        # if options are not required then this always succeeds
 | 
						|
        if not optionsRequired:
 | 
						|
            return True
 | 
						|
        # otherwise, if we have any option not present in other, then
 | 
						|
        # the match fails.
 | 
						|
        for opt in self.options:
 | 
						|
            if opt not in other.options:
 | 
						|
                return False
 | 
						|
        return True
 | 
						|
 | 
						|
            
 | 
						|
   
 | 
						|
    def __lt__(self, other):
 | 
						|
        return self.version < other.version or \
 | 
						|
               (self.version == other.version and self.options < other.options)
 | 
						|
    def __le__(self, other):
 | 
						|
        return self.version <= other.version or \
 | 
						|
               (self.version == other.version and self.options <= other.options)
 | 
						|
    
 | 
						|
    def __gt__(self, other):
 | 
						|
        return self.version > other.version or \
 | 
						|
               (self.version == other.version and self.options > other.options)
 | 
						|
    def __ge__(self, other):
 | 
						|
        return self.version >= other.version or \
 | 
						|
               (self.version == other.version and self.options >= other.options)
 | 
						|
    
 | 
						|
    def __eq__(self, other):
 | 
						|
        return self.version == other.version and self.options == other.options
 | 
						|
        
 | 
						|
    
 | 
						|
 | 
						|
#----------------------------------------------------------------------
 | 
						|
 | 
						|
if __name__ == '__main__':
 | 
						|
    import pprint
 | 
						|
 | 
						|
    #ensureMinimal('2.5')
 | 
						|
    #pprint.pprint(sys.path)
 | 
						|
    #sys.exit()
 | 
						|
    
 | 
						|
    
 | 
						|
    def test(version, optionsRequired=False):
 | 
						|
        # setup
 | 
						|
        savepath = sys.path[:]
 | 
						|
 | 
						|
        #test
 | 
						|
        select(version, optionsRequired)
 | 
						|
        print "Asked for %s, (%s):\t got: %s" % (version, optionsRequired, sys.path[0])
 | 
						|
 | 
						|
        # reset
 | 
						|
        sys.path = savepath[:]
 | 
						|
        global _selected
 | 
						|
        _selected = None
 | 
						|
 | 
						|
 | 
						|
    def testEM(version, optionsRequired=False):
 | 
						|
        # setup
 | 
						|
        savepath = sys.path[:]
 | 
						|
 | 
						|
        #test
 | 
						|
        ensureMinimal(version, optionsRequired)
 | 
						|
        print "EM: Asked for %s, (%s):\t got: %s" % (version, optionsRequired, sys.path[0])
 | 
						|
 | 
						|
        # reset
 | 
						|
        sys.path = savepath[:]
 | 
						|
        global _selected
 | 
						|
        _selected = None
 | 
						|
        
 | 
						|
        
 | 
						|
    # make some test dirs
 | 
						|
    names = ['wx-2.4-gtk-ansi',
 | 
						|
             'wx-2.5.2-gtk2-unicode',
 | 
						|
             'wx-2.5.3-gtk-ansi',
 | 
						|
             'wx-2.6-gtk2-unicode',
 | 
						|
             'wx-2.6-gtk2-ansi',
 | 
						|
             'wx-2.6-gtk-ansi',
 | 
						|
             'wx-2.7.1-gtk2-ansi',
 | 
						|
             ]
 | 
						|
    for name in names:
 | 
						|
        d = os.path.join('/tmp', name)
 | 
						|
        os.mkdir(d)
 | 
						|
        os.mkdir(os.path.join(d, 'wx'))
 | 
						|
 | 
						|
    # setup sys.path to see those dirs
 | 
						|
    sys.path.append('/tmp')
 | 
						|
    
 | 
						|
 | 
						|
    # now run some tests
 | 
						|
    pprint.pprint( getInstalled())
 | 
						|
    print checkInstalled("2.4")
 | 
						|
    print checkInstalled("2.5-unicode")
 | 
						|
    print checkInstalled("2.99-bogus")
 | 
						|
    print "Current sys.path:"
 | 
						|
    pprint.pprint(sys.path)
 | 
						|
    print
 | 
						|
    
 | 
						|
    test("2.4")
 | 
						|
    test("2.5")
 | 
						|
    test("2.5-gtk2")
 | 
						|
    test("2.5.2")
 | 
						|
    test("2.5-ansi")
 | 
						|
    test("2.5-unicode")
 | 
						|
    test("2.6")
 | 
						|
    test("2.6-ansi")
 | 
						|
    test(["2.6-unicode", "2.7-unicode"])
 | 
						|
    test(["2.6", "2.7"])
 | 
						|
    test(["2.6-unicode", "2.7-unicode"], optionsRequired=True)
 | 
						|
    
 | 
						|
    
 | 
						|
    
 | 
						|
    # There isn't a unicode match for this one, but it will give the best
 | 
						|
    # available 2.4.  Should it give an error instead?  I don't think so...
 | 
						|
    test("2.4-unicode") 
 | 
						|
 | 
						|
    # Try asking for multiple versions
 | 
						|
    test(["2.5.2", "2.5.3", "2.6"])
 | 
						|
 | 
						|
    try:
 | 
						|
        # expecting an error on this one
 | 
						|
        test("2.9")
 | 
						|
    except VersionError, e:
 | 
						|
        print "Asked for 2.9:\t got Exception:", e 
 | 
						|
 | 
						|
    # check for exception when incompatible versions are requested
 | 
						|
    try:
 | 
						|
        select("2.4")
 | 
						|
        select("2.5")
 | 
						|
    except VersionError, e:
 | 
						|
        print "Asked for incompatible versions, got Exception:", e 
 | 
						|
 | 
						|
    _EM_DEBUG=1
 | 
						|
    testEM("2.6")
 | 
						|
    testEM("2.6-unicode")
 | 
						|
    testEM("2.6-unicode", True)
 | 
						|
    try:
 | 
						|
        testEM("2.9")
 | 
						|
    except VersionError, e:
 | 
						|
        print "EM: Asked for 2.9:\t got Exception:", e 
 | 
						|
 | 
						|
    # cleanup
 | 
						|
    for name in names:
 | 
						|
        d = os.path.join('/tmp', name)
 | 
						|
        os.rmdir(os.path.join(d, 'wx'))
 | 
						|
        os.rmdir(d)
 | 
						|
 | 
						|
        
 |