]> git.saurik.com Git - wxWidgets.git/blame - wxPython/wxversion/wxversion.py
For whatever reason, font-size 9 is now hardly readable on Mac. Using default font...
[wxWidgets.git] / wxPython / wxversion / wxversion.py
CommitLineData
d48c1c64
RD
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"""
16If you have more than one version of wxPython installed this module
17allows your application to choose which version of wxPython will be
c08df249
RD
18imported when it does 'import wx'. The main function of this module
19is `select` and you use it like this::
d48c1c64
RD
20
21 import wxversion
5029118c
RD
22 wxversion.select('2.4')
23 import wx
24
c08df249
RD
25Or additional build options can also be selected, although they will
26not be required if they are not installed, like this::
5029118c
RD
27
28 import wxversion
29 wxversion.select('2.5.3-unicode')
d48c1c64
RD
30 import wx
31
c08df249
RD
32Or 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
38Finally you can also specify a collection of versions that are allowed
39by your application, like this::
40
41 import wxversion
42 wxversion.select(['2.5.4', '2.5.5', '2.6'])
43 import wx
44
45
d48c1c64
RD
46Of course the default wxPython version can also be controlled by
47setting PYTHONPATH or by editing the wx.pth path configuration file,
48but using wxversion will allow an application to manage the version
49selection itself rather than depend on the user to setup the
50environment correctly.
51
52It works by searching the sys.path for directories matching wx-* and
54c73383 53then comparing them to what was passed to the select function. If a
d48c1c64 54match is found then that path is inserted into sys.path.
5029118c
RD
55
56NOTE: If you are making a 'bundle' of your application with a tool
57like py2exe then you should *not* use the wxversion module since it
54c73383
RD
58looks at the filesystem for the directories on sys.path, it will fail
59in a bundled environment. Instead you should simply ensure that the
5029118c
RD
60version of wxPython that you want is found by default on the sys.path
61when making the bundled version by setting PYTHONPATH. Then that
62version will be included in your bundle and your app will work as
63expected. Py2exe and the others usually have a way to tell at runtime
64if they are running from a bundle or running raw, so you can check
65that 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
0148e434
RD
72More documentation on wxversion and multi-version installs can be
73found at: http://wiki.wxpython.org/index.cgi/MultiVersionInstalls
74
d48c1c64
RD
75"""
76
a9f3f5a6 77import re, sys, os, glob, fnmatch
d48c1c64
RD
78
79
4f60dce5 80_selected = None
5029118c 81class VersionError(Exception):
4f60dce5 82 pass
d48c1c64 83
5029118c 84#----------------------------------------------------------------------
d48c1c64 85
c08df249 86def select(versions, optionsRequired=False):
d48c1c64 87 """
5029118c
RD
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.
d48c1c64 93
c08df249
RD
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
d48c1c64 124 """
d48c1c64
RD
125 if type(versions) == str:
126 versions = [versions]
4f60dce5
RD
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:
c08df249 133 if _selected.Score(_wxPackageInfo(ver), optionsRequired) > 0:
4f60dce5
RD
134 return
135 # otherwise, raise an exception
5029118c 136 raise VersionError("A previously selected wx version does not match the new request.")
4f60dce5 137
0148e434
RD
138 # If we get here then this is the first time wxversion is used,
139 # ensure that wxPython hasn't been imported yet.
4f60dce5 140 if sys.modules.has_key('wx') or sys.modules.has_key('wxPython'):
54c73383
RD
141 raise VersionError("wxversion.select() must be called before wxPython is imported")
142
4f60dce5
RD
143 # Look for a matching version and manipulate the sys.path as
144 # needed to allow it to be imported.
54c73383 145 installed = _find_installed(True)
c08df249 146 bestMatch = _get_best_match(installed, versions, optionsRequired)
5029118c
RD
147
148 if bestMatch is None:
149 raise VersionError("Requested version of wxPython not found")
150
151 sys.path.insert(0, bestMatch.pathname)
a9f3f5a6
RD
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)
5029118c
RD
156 _selected = bestMatch
157
158#----------------------------------------------------------------------
159
5bb05e6d
RD
160UPDATE_URL = "http://wxPython.org/"
161#UPDATE_URL = "http://sourceforge.net/project/showfiles.php?group_id=10718"
162
c08df249 163_EM_DEBUG=0
6be3fd57 164
c08df249 165def ensureMinimal(minVersion, optionsRequired=False):
54c73383 166 """
6be3fd57
RD
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.
54c73383
RD
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'):
6be3fd57
RD
178 raise VersionError("wxversion.ensureMinimal() must be called before wxPython is imported")
179
54c73383 180 bestMatch = None
54c73383 181 minv = _wxPackageInfo(minVersion)
c08df249
RD
182
183 # check the default version first
6be3fd57
RD
184 defaultPath = _find_default()
185 if defaultPath:
186 defv = _wxPackageInfo(defaultPath, True)
c08df249 187 if defv >= minv and minv.CheckOptions(defv, optionsRequired):
6be3fd57 188 bestMatch = defv
54c73383 189
c08df249 190 # if still no match then check look at all installed versions
6be3fd57
RD
191 if bestMatch is None:
192 installed = _find_installed()
c08df249
RD
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
54c73383 202 if bestMatch is None:
c08df249
RD
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
54c73383
RD
206 import wx, webbrowser
207 versions = "\n".join([" "+ver for ver in getInstalled()])
208 app = wx.PySimpleApp()
6be3fd57
RD
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:
5bb05e6d 217 webbrowser.open(UPDATE_URL)
54c73383 218 app.MainLoop()
54c73383
RD
219 sys.exit()
220
221 sys.path.insert(0, bestMatch.pathname)
a9f3f5a6
RD
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)
ba705a4e 226 global _selected
54c73383
RD
227 _selected = bestMatch
228
229
230#----------------------------------------------------------------------
231
c08df249 232def checkInstalled(versions, optionsRequired=False):
5029118c
RD
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
c08df249
RD
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`.
5029118c
RD
242 """
243
244 if type(versions) == str:
245 versions = [versions]
54c73383 246 installed = _find_installed()
c08df249 247 bestMatch = _get_best_match(installed, versions, optionsRequired)
5029118c
RD
248 return bestMatch is not None
249
250#----------------------------------------------------------------------
251
252def getInstalled():
253 """
254 Returns a list of strings representing the installed wxPython
255 versions that are found on the system.
256 """
54c73383
RD
257 installed = _find_installed()
258 return [os.path.basename(p.pathname)[3:] for p in installed]
5029118c
RD
259
260
261
262#----------------------------------------------------------------------
263# private helpers...
264
c08df249 265def _get_best_match(installed, versions, optionsRequired):
5029118c
RD
266 bestMatch = None
267 bestScore = 0
54c73383 268 for pkg in installed:
d48c1c64 269 for ver in versions:
c08df249 270 score = pkg.Score(_wxPackageInfo(ver), optionsRequired)
d48c1c64
RD
271 if score > bestScore:
272 bestMatch = pkg
273 bestScore = score
5029118c 274 return bestMatch
d48c1c64
RD
275
276
277_pattern = "wx-[0-9].*"
5029118c 278def _find_installed(removeExisting=False):
d48c1c64 279 installed = []
17f3e530 280 toRemove = []
d48c1c64
RD
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
17f3e530
RD
293 # if it's a wx path that's already in the sys.path then mark
294 # it for removal and then skip it
d48c1c64 295 if fnmatch.fnmatchcase(base, _pattern):
17f3e530 296 toRemove.append(pth)
d48c1c64
RD
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
5029118c
RD
309 if removeExisting:
310 for rem in toRemove:
311 del sys.path[sys.path.index(rem)]
17f3e530 312
d48c1c64
RD
313 installed.sort()
314 installed.reverse()
315 return installed
316
317
6be3fd57
RD
318# Scan the sys.path looking for either a directory matching _pattern,
319# or a wx.pth file
320def _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
d48c1c64
RD
347class _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
c08df249 358 def Score(self, other, optionsRequired):
d48c1c64 359 score = 0
5029118c 360
0148e434
RD
361 # whatever number of version components given in other must
362 # match exactly
5029118c
RD
363 minlen = min(len(self.version), len(other.version))
364 if self.version[:minlen] != other.version[:minlen]:
365 return 0
0148e434 366 score += 1
c08df249
RD
367
368 # check for matching options, if optionsRequired then the
369 # options are not optional ;-)
d48c1c64
RD
370 for opt in other.options:
371 if opt in self.options:
372 score += 1
c08df249
RD
373 elif optionsRequired:
374 return 0
375
d48c1c64 376 return score
d48c1c64 377
c08df249
RD
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
54c73383 391
d48c1c64 392 def __lt__(self, other):
54c73383
RD
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
d48c1c64 399 def __gt__(self, other):
54c73383
RD
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
d48c1c64 406 def __eq__(self, other):
54c73383 407 return self.version == other.version and self.options == other.options
d48c1c64
RD
408
409
410
5029118c 411#----------------------------------------------------------------------
d48c1c64
RD
412
413if __name__ == '__main__':
17f3e530 414 import pprint
6be3fd57
RD
415
416 #ensureMinimal('2.5')
417 #pprint.pprint(sys.path)
54c73383
RD
418 #sys.exit()
419
420
c08df249 421 def test(version, optionsRequired=False):
4f60dce5 422 # setup
d48c1c64 423 savepath = sys.path[:]
4f60dce5
RD
424
425 #test
c08df249
RD
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])
4f60dce5
RD
442
443 # reset
d48c1c64 444 sys.path = savepath[:]
4f60dce5
RD
445 global _selected
446 _selected = None
d48c1c64
RD
447
448
449 # make some test dirs
c08df249
RD
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 ]
d48c1c64
RD
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
5029118c
RD
468 pprint.pprint( getInstalled())
469 print checkInstalled("2.4")
470 print checkInstalled("2.5-unicode")
471 print checkInstalled("2.99-bogus")
6be3fd57
RD
472 print "Current sys.path:"
473 pprint.pprint(sys.path)
474 print
475
d48c1c64
RD
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")
c08df249
RD
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
d48c1c64
RD
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
4f60dce5 494 # Try asking for multiple versions
c08df249 495 test(["2.5.2", "2.5.3", "2.6"])
4f60dce5 496
d48c1c64
RD
497 try:
498 # expecting an error on this one
c08df249 499 test("2.9")
5029118c 500 except VersionError, e:
c08df249 501 print "Asked for 2.9:\t got Exception:", e
d48c1c64 502
4f60dce5
RD
503 # check for exception when incompatible versions are requested
504 try:
5029118c
RD
505 select("2.4")
506 select("2.5")
507 except VersionError, e:
4f60dce5 508 print "Asked for incompatible versions, got Exception:", e
d48c1c64 509
c08df249
RD
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
d48c1c64
RD
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