| 1 | #---------------------------------------------------------------------- |
| 2 | # Name: wxversion |
| 3 | # Purpose: Allows a wxPython program to search for alternate |
| 4 | # installations of the wxPython packages and modify sys.path |
| 5 | # so they will be found when "import wx" is done. |
| 6 | # |
| 7 | # Author: Robin Dunn |
| 8 | # |
| 9 | # Created: 24-Sept-2004 |
| 10 | # RCS-ID: $Id$ |
| 11 | # Copyright: (c) 2004 by Total Control Software |
| 12 | # Licence: wxWindows license |
| 13 | #---------------------------------------------------------------------- |
| 14 | |
| 15 | """ |
| 16 | If you have more than one version of wxPython installed this module |
| 17 | allows your application to choose which version of wxPython will be |
| 18 | imported when it does 'import wx'. The main function of this module |
| 19 | is `select` and you use it like this:: |
| 20 | |
| 21 | import wxversion |
| 22 | wxversion.select('2.4') |
| 23 | import wx |
| 24 | |
| 25 | Or additional build options can also be selected, although they will |
| 26 | not be required if they are not installed, like this:: |
| 27 | |
| 28 | import wxversion |
| 29 | wxversion.select('2.5.3-unicode') |
| 30 | import wx |
| 31 | |
| 32 | Or you can require an exact match on the build options like this:: |
| 33 | |
| 34 | import wxversion |
| 35 | wxversion.select('2.5.3-unicode', optionsRequired=True) |
| 36 | import wx |
| 37 | |
| 38 | Finally you can also specify a collection of versions that are allowed |
| 39 | by your application, like this:: |
| 40 | |
| 41 | import wxversion |
| 42 | wxversion.select(['2.5.4', '2.5.5', '2.6']) |
| 43 | import wx |
| 44 | |
| 45 | |
| 46 | Of course the default wxPython version can also be controlled by |
| 47 | setting PYTHONPATH or by editing the wx.pth path configuration file, |
| 48 | but using wxversion will allow an application to manage the version |
| 49 | selection itself rather than depend on the user to setup the |
| 50 | environment correctly. |
| 51 | |
| 52 | It works by searching the sys.path for directories matching wx-* and |
| 53 | then comparing them to what was passed to the select function. If a |
| 54 | match is found then that path is inserted into sys.path. |
| 55 | |
| 56 | NOTE: If you are making a 'bundle' of your application with a tool |
| 57 | like py2exe then you should *not* use the wxversion module since it |
| 58 | looks at the filesystem for the directories on sys.path, it will fail |
| 59 | in a bundled environment. Instead you should simply ensure that the |
| 60 | version of wxPython that you want is found by default on the sys.path |
| 61 | when making the bundled version by setting PYTHONPATH. Then that |
| 62 | version will be included in your bundle and your app will work as |
| 63 | expected. Py2exe and the others usually have a way to tell at runtime |
| 64 | if they are running from a bundle or running raw, so you can check |
| 65 | that and only use wxversion if needed. For example, for py2exe:: |
| 66 | |
| 67 | if not hasattr(sys, 'frozen'): |
| 68 | import wxversion |
| 69 | wxversion.select('2.5') |
| 70 | import wx |
| 71 | |
| 72 | More documentation on wxversion and multi-version installs can be |
| 73 | found at: http://wiki.wxpython.org/index.cgi/MultiVersionInstalls |
| 74 | |
| 75 | """ |
| 76 | |
| 77 | import re, sys, os, glob, fnmatch |
| 78 | |
| 79 | |
| 80 | _selected = None |
| 81 | class VersionError(Exception): |
| 82 | pass |
| 83 | |
| 84 | #---------------------------------------------------------------------- |
| 85 | |
| 86 | def select(versions, optionsRequired=False): |
| 87 | """ |
| 88 | Search for a wxPython installation that matches version. If one |
| 89 | is found then sys.path is modified so that version will be |
| 90 | imported with a 'import wx', otherwise a VersionError exception is |
| 91 | raised. This funciton should only be caled once at the begining |
| 92 | of the application before wxPython is imported. |
| 93 | |
| 94 | :param versions: Specifies the version to look for, it can |
| 95 | either be a string or a list of strings. Each string is |
| 96 | compared to the installed wxPythons and the best match is |
| 97 | inserted into the sys.path, allowing an 'import wx' to |
| 98 | find that version. |
| 99 | |
| 100 | The version string is composed of the dotted version |
| 101 | number (at least 2 of the 4 components) optionally |
| 102 | followed by hyphen ('-') separated options (wx port, |
| 103 | unicode/ansi, flavour, etc.) A match is determined by how |
| 104 | much of the installed version matches what is given in the |
| 105 | version parameter. If the version number components don't |
| 106 | match then the score is zero, otherwise the score is |
| 107 | increased for every specified optional component that is |
| 108 | specified and that matches. |
| 109 | |
| 110 | Please note, however, that it is possible for a match to |
| 111 | be selected that doesn't exactly match the versions |
| 112 | requested. The only component that is required to be |
| 113 | matched is the version number. If you need to require a |
| 114 | match on the other components as well, then please use the |
| 115 | optional ``optionsRequired`` parameter described next. |
| 116 | |
| 117 | :param optionsRequired: Allows you to specify that the other |
| 118 | components of the version string (such as the port name |
| 119 | or character type) are also required to be present for an |
| 120 | installed version to be considered a match. Using this |
| 121 | parameter allows you to change the selection from a soft, |
| 122 | as close as possible match to a hard, exact match. |
| 123 | |
| 124 | """ |
| 125 | if type(versions) == str: |
| 126 | versions = [versions] |
| 127 | |
| 128 | global _selected |
| 129 | if _selected is not None: |
| 130 | # A version was previously selected, ensure that it matches |
| 131 | # this new request |
| 132 | for ver in versions: |
| 133 | if _selected.Score(_wxPackageInfo(ver), optionsRequired) > 0: |
| 134 | return |
| 135 | # otherwise, raise an exception |
| 136 | raise VersionError("A previously selected wx version does not match the new request.") |
| 137 | |
| 138 | # If we get here then this is the first time wxversion is used, |
| 139 | # ensure that wxPython hasn't been imported yet. |
| 140 | if sys.modules.has_key('wx') or sys.modules.has_key('wxPython'): |
| 141 | raise VersionError("wxversion.select() must be called before wxPython is imported") |
| 142 | |
| 143 | # Look for a matching version and manipulate the sys.path as |
| 144 | # needed to allow it to be imported. |
| 145 | installed = _find_installed(True) |
| 146 | bestMatch = _get_best_match(installed, versions, optionsRequired) |
| 147 | |
| 148 | if bestMatch is None: |
| 149 | raise VersionError("Requested version of wxPython not found") |
| 150 | |
| 151 | sys.path.insert(0, bestMatch.pathname) |
| 152 | # q.v. Bug #1409256 |
| 153 | path64 = re.sub('/lib/','/lib64/',bestMatch.pathname) |
| 154 | if os.path.isdir(path64): |
| 155 | sys.path.insert(0, path64) |
| 156 | _selected = bestMatch |
| 157 | |
| 158 | #---------------------------------------------------------------------- |
| 159 | |
| 160 | UPDATE_URL = "http://wxPython.org/" |
| 161 | #UPDATE_URL = "http://sourceforge.net/project/showfiles.php?group_id=10718" |
| 162 | |
| 163 | _EM_DEBUG=0 |
| 164 | |
| 165 | def ensureMinimal(minVersion, optionsRequired=False): |
| 166 | """ |
| 167 | Checks to see if the default version of wxPython is greater-than |
| 168 | or equal to `minVersion`. If not then it will try to find an |
| 169 | installed version that is >= minVersion. If none are available |
| 170 | then a message is displayed that will inform the user and will |
| 171 | offer to open their web browser to the wxPython downloads page, |
| 172 | and will then exit the application. |
| 173 | """ |
| 174 | assert type(minVersion) == str |
| 175 | |
| 176 | # ensure that wxPython hasn't been imported yet. |
| 177 | if sys.modules.has_key('wx') or sys.modules.has_key('wxPython'): |
| 178 | raise VersionError("wxversion.ensureMinimal() must be called before wxPython is imported") |
| 179 | |
| 180 | bestMatch = None |
| 181 | minv = _wxPackageInfo(minVersion) |
| 182 | |
| 183 | # check the default version first |
| 184 | defaultPath = _find_default() |
| 185 | if defaultPath: |
| 186 | defv = _wxPackageInfo(defaultPath, True) |
| 187 | if defv >= minv and minv.CheckOptions(defv, optionsRequired): |
| 188 | bestMatch = defv |
| 189 | |
| 190 | # if still no match then check look at all installed versions |
| 191 | if bestMatch is None: |
| 192 | installed = _find_installed() |
| 193 | # The list is in reverse sorted order, so find the first |
| 194 | # one that is big enough and optionally matches the |
| 195 | # options |
| 196 | for inst in installed: |
| 197 | if inst >= minv and minv.CheckOptions(inst, optionsRequired): |
| 198 | bestMatch = inst |
| 199 | break |
| 200 | |
| 201 | # if still no match then prompt the user |
| 202 | if bestMatch is None: |
| 203 | if _EM_DEBUG: # We'll do it this way just for the test code below |
| 204 | raise VersionError("Requested version of wxPython not found") |
| 205 | |
| 206 | import wx, webbrowser |
| 207 | versions = "\n".join([" "+ver for ver in getInstalled()]) |
| 208 | app = wx.PySimpleApp() |
| 209 | result = wx.MessageBox("This application requires a version of wxPython " |
| 210 | "greater than or equal to %s, but a matching version " |
| 211 | "was not found.\n\n" |
| 212 | "You currently have these version(s) installed:\n%s\n\n" |
| 213 | "Would you like to download a new version of wxPython?\n" |
| 214 | % (minVersion, versions), |
| 215 | "wxPython Upgrade Needed", style=wx.YES_NO) |
| 216 | if result == wx.YES: |
| 217 | webbrowser.open(UPDATE_URL) |
| 218 | app.MainLoop() |
| 219 | sys.exit() |
| 220 | |
| 221 | sys.path.insert(0, bestMatch.pathname) |
| 222 | # q.v. Bug #1409256 |
| 223 | path64 = re.sub('/lib/','/lib64/',bestMatch.pathname) |
| 224 | if os.path.isdir(path64): |
| 225 | sys.path.insert(0, path64) |
| 226 | global _selected |
| 227 | _selected = bestMatch |
| 228 | |
| 229 | |
| 230 | #---------------------------------------------------------------------- |
| 231 | |
| 232 | def checkInstalled(versions, optionsRequired=False): |
| 233 | """ |
| 234 | Check if there is a version of wxPython installed that matches one |
| 235 | of the versions given. Returns True if so, False if not. This |
| 236 | can be used to determine if calling `select` will succeed or not. |
| 237 | |
| 238 | :param versions: Same as in `select`, either a string or a list |
| 239 | of strings specifying the version(s) to check for. |
| 240 | |
| 241 | :param optionsRequired: Same as in `select`. |
| 242 | """ |
| 243 | |
| 244 | if type(versions) == str: |
| 245 | versions = [versions] |
| 246 | installed = _find_installed() |
| 247 | bestMatch = _get_best_match(installed, versions, optionsRequired) |
| 248 | return bestMatch is not None |
| 249 | |
| 250 | #---------------------------------------------------------------------- |
| 251 | |
| 252 | def getInstalled(): |
| 253 | """ |
| 254 | Returns a list of strings representing the installed wxPython |
| 255 | versions that are found on the system. |
| 256 | """ |
| 257 | installed = _find_installed() |
| 258 | return [os.path.basename(p.pathname)[3:] for p in installed] |
| 259 | |
| 260 | |
| 261 | |
| 262 | #---------------------------------------------------------------------- |
| 263 | # private helpers... |
| 264 | |
| 265 | def _get_best_match(installed, versions, optionsRequired): |
| 266 | bestMatch = None |
| 267 | bestScore = 0 |
| 268 | for pkg in installed: |
| 269 | for ver in versions: |
| 270 | score = pkg.Score(_wxPackageInfo(ver), optionsRequired) |
| 271 | if score > bestScore: |
| 272 | bestMatch = pkg |
| 273 | bestScore = score |
| 274 | return bestMatch |
| 275 | |
| 276 | |
| 277 | _pattern = "wx-[0-9].*" |
| 278 | def _find_installed(removeExisting=False): |
| 279 | installed = [] |
| 280 | toRemove = [] |
| 281 | for pth in sys.path: |
| 282 | |
| 283 | # empty means to look in the current dir |
| 284 | if not pth: |
| 285 | pth = '.' |
| 286 | |
| 287 | # skip it if it's not a package dir |
| 288 | if not os.path.isdir(pth): |
| 289 | continue |
| 290 | |
| 291 | base = os.path.basename(pth) |
| 292 | |
| 293 | # if it's a wx path that's already in the sys.path then mark |
| 294 | # it for removal and then skip it |
| 295 | if fnmatch.fnmatchcase(base, _pattern): |
| 296 | toRemove.append(pth) |
| 297 | continue |
| 298 | |
| 299 | # now look in the dir for matching subdirs |
| 300 | for name in glob.glob(os.path.join(pth, _pattern)): |
| 301 | # make sure it's a directory |
| 302 | if not os.path.isdir(name): |
| 303 | continue |
| 304 | # and has a wx subdir |
| 305 | if not os.path.exists(os.path.join(name, 'wx')): |
| 306 | continue |
| 307 | installed.append(_wxPackageInfo(name, True)) |
| 308 | |
| 309 | if removeExisting: |
| 310 | for rem in toRemove: |
| 311 | del sys.path[sys.path.index(rem)] |
| 312 | |
| 313 | installed.sort() |
| 314 | installed.reverse() |
| 315 | return installed |
| 316 | |
| 317 | |
| 318 | # Scan the sys.path looking for either a directory matching _pattern, |
| 319 | # or a wx.pth file |
| 320 | def _find_default(): |
| 321 | for pth in sys.path: |
| 322 | # empty means to look in the current dir |
| 323 | if not pth: |
| 324 | pth = '.' |
| 325 | |
| 326 | # skip it if it's not a package dir |
| 327 | if not os.path.isdir(pth): |
| 328 | continue |
| 329 | |
| 330 | # does it match the pattern? |
| 331 | base = os.path.basename(pth) |
| 332 | if fnmatch.fnmatchcase(base, _pattern): |
| 333 | return pth |
| 334 | |
| 335 | for pth in sys.path: |
| 336 | if not pth: |
| 337 | pth = '.' |
| 338 | if not os.path.isdir(pth): |
| 339 | continue |
| 340 | if os.path.exists(os.path.join(pth, 'wx.pth')): |
| 341 | base = open(os.path.join(pth, 'wx.pth')).read() |
| 342 | return os.path.join(pth, base) |
| 343 | |
| 344 | return None |
| 345 | |
| 346 | |
| 347 | class _wxPackageInfo(object): |
| 348 | def __init__(self, pathname, stripFirst=False): |
| 349 | self.pathname = pathname |
| 350 | base = os.path.basename(pathname) |
| 351 | segments = base.split('-') |
| 352 | if stripFirst: |
| 353 | segments = segments[1:] |
| 354 | self.version = tuple([int(x) for x in segments[0].split('.')]) |
| 355 | self.options = segments[1:] |
| 356 | |
| 357 | |
| 358 | def Score(self, other, optionsRequired): |
| 359 | score = 0 |
| 360 | |
| 361 | # whatever number of version components given in other must |
| 362 | # match exactly |
| 363 | minlen = min(len(self.version), len(other.version)) |
| 364 | if self.version[:minlen] != other.version[:minlen]: |
| 365 | return 0 |
| 366 | score += 1 |
| 367 | |
| 368 | # check for matching options, if optionsRequired then the |
| 369 | # options are not optional ;-) |
| 370 | for opt in other.options: |
| 371 | if opt in self.options: |
| 372 | score += 1 |
| 373 | elif optionsRequired: |
| 374 | return 0 |
| 375 | |
| 376 | return score |
| 377 | |
| 378 | |
| 379 | def CheckOptions(self, other, optionsRequired): |
| 380 | # if options are not required then this always succeeds |
| 381 | if not optionsRequired: |
| 382 | return True |
| 383 | # otherwise, if we have any option not present in other, then |
| 384 | # the match fails. |
| 385 | for opt in self.options: |
| 386 | if opt not in other.options: |
| 387 | return False |
| 388 | return True |
| 389 | |
| 390 | |
| 391 | |
| 392 | def __lt__(self, other): |
| 393 | return self.version < other.version or \ |
| 394 | (self.version == other.version and self.options < other.options) |
| 395 | def __le__(self, other): |
| 396 | return self.version <= other.version or \ |
| 397 | (self.version == other.version and self.options <= other.options) |
| 398 | |
| 399 | def __gt__(self, other): |
| 400 | return self.version > other.version or \ |
| 401 | (self.version == other.version and self.options > other.options) |
| 402 | def __ge__(self, other): |
| 403 | return self.version >= other.version or \ |
| 404 | (self.version == other.version and self.options >= other.options) |
| 405 | |
| 406 | def __eq__(self, other): |
| 407 | return self.version == other.version and self.options == other.options |
| 408 | |
| 409 | |
| 410 | |
| 411 | #---------------------------------------------------------------------- |
| 412 | |
| 413 | if __name__ == '__main__': |
| 414 | import pprint |
| 415 | |
| 416 | #ensureMinimal('2.5') |
| 417 | #pprint.pprint(sys.path) |
| 418 | #sys.exit() |
| 419 | |
| 420 | |
| 421 | def test(version, optionsRequired=False): |
| 422 | # setup |
| 423 | savepath = sys.path[:] |
| 424 | |
| 425 | #test |
| 426 | select(version, optionsRequired) |
| 427 | print "Asked for %s, (%s):\t got: %s" % (version, optionsRequired, sys.path[0]) |
| 428 | |
| 429 | # reset |
| 430 | sys.path = savepath[:] |
| 431 | global _selected |
| 432 | _selected = None |
| 433 | |
| 434 | |
| 435 | def testEM(version, optionsRequired=False): |
| 436 | # setup |
| 437 | savepath = sys.path[:] |
| 438 | |
| 439 | #test |
| 440 | ensureMinimal(version, optionsRequired) |
| 441 | print "EM: Asked for %s, (%s):\t got: %s" % (version, optionsRequired, sys.path[0]) |
| 442 | |
| 443 | # reset |
| 444 | sys.path = savepath[:] |
| 445 | global _selected |
| 446 | _selected = None |
| 447 | |
| 448 | |
| 449 | # make some test dirs |
| 450 | names = ['wx-2.4-gtk-ansi', |
| 451 | 'wx-2.5.2-gtk2-unicode', |
| 452 | 'wx-2.5.3-gtk-ansi', |
| 453 | 'wx-2.6-gtk2-unicode', |
| 454 | 'wx-2.6-gtk2-ansi', |
| 455 | 'wx-2.6-gtk-ansi', |
| 456 | 'wx-2.7.1-gtk2-ansi', |
| 457 | ] |
| 458 | for name in names: |
| 459 | d = os.path.join('/tmp', name) |
| 460 | os.mkdir(d) |
| 461 | os.mkdir(os.path.join(d, 'wx')) |
| 462 | |
| 463 | # setup sys.path to see those dirs |
| 464 | sys.path.append('/tmp') |
| 465 | |
| 466 | |
| 467 | # now run some tests |
| 468 | pprint.pprint( getInstalled()) |
| 469 | print checkInstalled("2.4") |
| 470 | print checkInstalled("2.5-unicode") |
| 471 | print checkInstalled("2.99-bogus") |
| 472 | print "Current sys.path:" |
| 473 | pprint.pprint(sys.path) |
| 474 | print |
| 475 | |
| 476 | test("2.4") |
| 477 | test("2.5") |
| 478 | test("2.5-gtk2") |
| 479 | test("2.5.2") |
| 480 | test("2.5-ansi") |
| 481 | test("2.5-unicode") |
| 482 | test("2.6") |
| 483 | test("2.6-ansi") |
| 484 | test(["2.6-unicode", "2.7-unicode"]) |
| 485 | test(["2.6", "2.7"]) |
| 486 | test(["2.6-unicode", "2.7-unicode"], optionsRequired=True) |
| 487 | |
| 488 | |
| 489 | |
| 490 | # There isn't a unicode match for this one, but it will give the best |
| 491 | # available 2.4. Should it give an error instead? I don't think so... |
| 492 | test("2.4-unicode") |
| 493 | |
| 494 | # Try asking for multiple versions |
| 495 | test(["2.5.2", "2.5.3", "2.6"]) |
| 496 | |
| 497 | try: |
| 498 | # expecting an error on this one |
| 499 | test("2.9") |
| 500 | except VersionError, e: |
| 501 | print "Asked for 2.9:\t got Exception:", e |
| 502 | |
| 503 | # check for exception when incompatible versions are requested |
| 504 | try: |
| 505 | select("2.4") |
| 506 | select("2.5") |
| 507 | except VersionError, e: |
| 508 | print "Asked for incompatible versions, got Exception:", e |
| 509 | |
| 510 | _EM_DEBUG=1 |
| 511 | testEM("2.6") |
| 512 | testEM("2.6-unicode") |
| 513 | testEM("2.6-unicode", True) |
| 514 | try: |
| 515 | testEM("2.9") |
| 516 | except VersionError, e: |
| 517 | print "EM: Asked for 2.9:\t got Exception:", e |
| 518 | |
| 519 | # cleanup |
| 520 | for name in names: |
| 521 | d = os.path.join('/tmp', name) |
| 522 | os.rmdir(os.path.join(d, 'wx')) |
| 523 | os.rmdir(d) |
| 524 | |
| 525 | |