]> git.saurik.com Git - wxWidgets.git/blobdiff - wxPython/wxversion/wxversion.py
added a test to check how exception handling works with Win32 SEH; corrected copyright
[wxWidgets.git] / wxPython / wxversion / wxversion.py
index 5b9eb6e349f4a40d93e73340e035ca60f6503c5c..b024966706d244e548399de6d548cf1faae1a822 100644 (file)
 """
 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'.  You use it like this:
+imported when it does 'import wx'.  You use it like this::
 
     import wxversion
-    wxversion.require('2.4')
+    wxversion.select('2.4')
+    import wx
+
+Or additional build options can also be selected, like this::
+
+    import wxversion
+    wxversion.select('2.5.3-unicode')
     import wx
 
 Of course the default wxPython version can also be controlled by
@@ -28,18 +34,46 @@ 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 require function.  If a
+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 sys, os, glob, fnmatch
 
 
+_selected = None
+class VersionError(Exception):
+    pass
 
+#----------------------------------------------------------------------
 
-def require(versions):
+def select(versions):
     """
-    Search for a wxPython installation that matches version.
+    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 version: Specifies the version to look for, it can
                         either be a string or a list of strings.  Each
@@ -59,32 +93,140 @@ def require(versions):
                         is increased for every specified optional component
                         that is specified and that matches.
     """
-    assert not sys.modules.has_key('wx') and not sys.modules.has_key('wxPython'), \
-           "wxversion.require() must be called before wxPython is imported"
+    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)) > 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)
+    
+    if bestMatch is None:
+        raise VersionError("Requested version of wxPython not found")
+
+    sys.path.insert(0, bestMatch.pathname)
+    _selected = bestMatch
+        
+#----------------------------------------------------------------------
+
+UPDATE_URL = "http://wxPython.org/"
+#UPDATE_URL = "http://sourceforge.net/project/showfiles.php?group_id=10718"
+
+
+def ensureMinimal(minVersion):
+    """
+    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
-    bestScore = 0
+    minv = _wxPackageInfo(minVersion)
+    defaultPath = _find_default()
+    if defaultPath:
+        defv = _wxPackageInfo(defaultPath, True)
+        if defv >= minv:
+            bestMatch = defv
+
+    if bestMatch is None:
+        installed = _find_installed()
+        if installed:
+            # The list is in reverse sorted order, so if the first one is
+            # big enough then choose it
+            if installed[0] >= minv:
+                bestMatch = installed[0]
+
+    if bestMatch is None:
+        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)
+    _selected = bestMatch
+        
+
+#----------------------------------------------------------------------
+
+def checkInstalled(versions):
+    """
+    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 version: Same as in `select`, either a string or a list
+                        of strings specifying the version(s) to check
+                        for.    
+    """
+    
     if type(versions) == str:
         versions = [versions]
-        
-    packages = _find_installed()
-    for pkg in packages:
+    installed = _find_installed()
+    bestMatch = _get_best_match(installed, versions)
+    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):
+    bestMatch = None
+    bestScore = 0
+    for pkg in installed:
         for ver in versions:
             score = pkg.Score(_wxPackageInfo(ver))
             if score > bestScore:
                 bestMatch = pkg
                 bestScore = score
-
-    assert bestMatch is not None, \
-           "Required version of wxPython not found"
-
-    sys.path.insert(0, bestMatch.pathname)
-        
-        
+    return bestMatch
 
 
 _pattern = "wx-[0-9].*"
-def _find_installed():
+def _find_installed(removeExisting=False):
     installed = []
     toRemove = []
     for pth in sys.path:
@@ -115,14 +257,44 @@ def _find_installed():
                 continue
             installed.append(_wxPackageInfo(name, True))
 
-    for rem in toRemove:
-        del sys.path[sys.path.index(rem)]
+    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
@@ -136,41 +308,64 @@ class _wxPackageInfo(object):
 
     def Score(self, other):
         score = 0
-        # whatever version components given in other must match exactly
-        if len(self.version) > len(other.version):
-            v = self.version[:len(other.version)]
-        else:
-            v = self.version
-        if v != other.version:
-            return 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
+        
         for opt in other.options:
             if opt in self.options:
                 score += 1
         return score
     
 
-    # TODO: factor self.options into the sort order?
+   
     def __lt__(self, other):
-        return self.version < other.version
+        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
+        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
+        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):
+        # setup
         savepath = sys.path[:]
-        require(version)
+
+        #test
+        select(version)
         print "Asked for %s:\t got: %s" % (version, sys.path[0])
         pprint.pprint(sys.path)
         print
+
+        # reset
         sys.path = savepath[:]
+        global _selected
+        _selected = None
         
         
     # make some test dirs
@@ -191,6 +386,14 @@ if __name__ == '__main__':
     
 
     # 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")
@@ -202,14 +405,21 @@ if __name__ == '__main__':
     # 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.6", "2.5.3", "2.5.2-gtk2"])
+
     try:
         # expecting an error on this one
         test("2.6")
-    except AssertionError:
-        print "Asked for 2.6:\t got: Assertion" 
+    except VersionError, e:
+        print "Asked for 2.6:\t got Exception:", e 
 
-    # Try asking for multiple versions
-    test(["2.6", "2.5.3", "2.5.2-gtk2"])
+    # 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 
 
     # cleanup
     for name in names: