]> git.saurik.com Git - wxWidgets.git/blob - wxPython/wxversion/wxversion.py
correction to exception handling in threads: call wxThread::OnExit() always
[wxWidgets.git] / wxPython / wxversion / wxversion.py
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