]> git.saurik.com Git - wxWidgets.git/blame - wxPython/wxversion/wxversion.py
updates
[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
77import sys, os, glob, fnmatch
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)
152 _selected = bestMatch
153
154#----------------------------------------------------------------------
155
5bb05e6d
RD
156UPDATE_URL = "http://wxPython.org/"
157#UPDATE_URL = "http://sourceforge.net/project/showfiles.php?group_id=10718"
158
c08df249 159_EM_DEBUG=0
6be3fd57 160
c08df249 161def ensureMinimal(minVersion, optionsRequired=False):
54c73383 162 """
6be3fd57
RD
163 Checks to see if the default version of wxPython is greater-than
164 or equal to `minVersion`. If not then it will try to find an
165 installed version that is >= minVersion. If none are available
166 then a message is displayed that will inform the user and will
167 offer to open their web browser to the wxPython downloads page,
168 and will then exit the application.
54c73383
RD
169 """
170 assert type(minVersion) == str
171
172 # ensure that wxPython hasn't been imported yet.
173 if sys.modules.has_key('wx') or sys.modules.has_key('wxPython'):
6be3fd57
RD
174 raise VersionError("wxversion.ensureMinimal() must be called before wxPython is imported")
175
54c73383 176 bestMatch = None
54c73383 177 minv = _wxPackageInfo(minVersion)
c08df249
RD
178
179 # check the default version first
6be3fd57
RD
180 defaultPath = _find_default()
181 if defaultPath:
182 defv = _wxPackageInfo(defaultPath, True)
c08df249 183 if defv >= minv and minv.CheckOptions(defv, optionsRequired):
6be3fd57 184 bestMatch = defv
54c73383 185
c08df249 186 # if still no match then check look at all installed versions
6be3fd57
RD
187 if bestMatch is None:
188 installed = _find_installed()
c08df249
RD
189 # The list is in reverse sorted order, so find the first
190 # one that is big enough and optionally matches the
191 # options
192 for inst in installed:
193 if inst >= minv and minv.CheckOptions(inst, optionsRequired):
194 bestMatch = inst
195 break
196
197 # if still no match then prompt the user
54c73383 198 if bestMatch is None:
c08df249
RD
199 if _EM_DEBUG: # We'll do it this way just for the test code below
200 raise VersionError("Requested version of wxPython not found")
201
54c73383
RD
202 import wx, webbrowser
203 versions = "\n".join([" "+ver for ver in getInstalled()])
204 app = wx.PySimpleApp()
6be3fd57
RD
205 result = wx.MessageBox("This application requires a version of wxPython "
206 "greater than or equal to %s, but a matching version "
207 "was not found.\n\n"
208 "You currently have these version(s) installed:\n%s\n\n"
209 "Would you like to download a new version of wxPython?\n"
210 % (minVersion, versions),
211 "wxPython Upgrade Needed", style=wx.YES_NO)
212 if result == wx.YES:
5bb05e6d 213 webbrowser.open(UPDATE_URL)
54c73383 214 app.MainLoop()
54c73383
RD
215 sys.exit()
216
217 sys.path.insert(0, bestMatch.pathname)
ba705a4e 218 global _selected
54c73383
RD
219 _selected = bestMatch
220
221
222#----------------------------------------------------------------------
223
c08df249 224def checkInstalled(versions, optionsRequired=False):
5029118c
RD
225 """
226 Check if there is a version of wxPython installed that matches one
227 of the versions given. Returns True if so, False if not. This
228 can be used to determine if calling `select` will succeed or not.
229
c08df249
RD
230 :param versions: Same as in `select`, either a string or a list
231 of strings specifying the version(s) to check for.
232
233 :param optionsRequired: Same as in `select`.
5029118c
RD
234 """
235
236 if type(versions) == str:
237 versions = [versions]
54c73383 238 installed = _find_installed()
c08df249 239 bestMatch = _get_best_match(installed, versions, optionsRequired)
5029118c
RD
240 return bestMatch is not None
241
242#----------------------------------------------------------------------
243
244def getInstalled():
245 """
246 Returns a list of strings representing the installed wxPython
247 versions that are found on the system.
248 """
54c73383
RD
249 installed = _find_installed()
250 return [os.path.basename(p.pathname)[3:] for p in installed]
5029118c
RD
251
252
253
254#----------------------------------------------------------------------
255# private helpers...
256
c08df249 257def _get_best_match(installed, versions, optionsRequired):
5029118c
RD
258 bestMatch = None
259 bestScore = 0
54c73383 260 for pkg in installed:
d48c1c64 261 for ver in versions:
c08df249 262 score = pkg.Score(_wxPackageInfo(ver), optionsRequired)
d48c1c64
RD
263 if score > bestScore:
264 bestMatch = pkg
265 bestScore = score
5029118c 266 return bestMatch
d48c1c64
RD
267
268
269_pattern = "wx-[0-9].*"
5029118c 270def _find_installed(removeExisting=False):
d48c1c64 271 installed = []
17f3e530 272 toRemove = []
d48c1c64
RD
273 for pth in sys.path:
274
275 # empty means to look in the current dir
276 if not pth:
277 pth = '.'
278
279 # skip it if it's not a package dir
280 if not os.path.isdir(pth):
281 continue
282
283 base = os.path.basename(pth)
284
17f3e530
RD
285 # if it's a wx path that's already in the sys.path then mark
286 # it for removal and then skip it
d48c1c64 287 if fnmatch.fnmatchcase(base, _pattern):
17f3e530 288 toRemove.append(pth)
d48c1c64
RD
289 continue
290
291 # now look in the dir for matching subdirs
292 for name in glob.glob(os.path.join(pth, _pattern)):
293 # make sure it's a directory
294 if not os.path.isdir(name):
295 continue
296 # and has a wx subdir
297 if not os.path.exists(os.path.join(name, 'wx')):
298 continue
299 installed.append(_wxPackageInfo(name, True))
300
5029118c
RD
301 if removeExisting:
302 for rem in toRemove:
303 del sys.path[sys.path.index(rem)]
17f3e530 304
d48c1c64
RD
305 installed.sort()
306 installed.reverse()
307 return installed
308
309
6be3fd57
RD
310# Scan the sys.path looking for either a directory matching _pattern,
311# or a wx.pth file
312def _find_default():
313 for pth in sys.path:
314 # empty means to look in the current dir
315 if not pth:
316 pth = '.'
317
318 # skip it if it's not a package dir
319 if not os.path.isdir(pth):
320 continue
321
322 # does it match the pattern?
323 base = os.path.basename(pth)
324 if fnmatch.fnmatchcase(base, _pattern):
325 return pth
326
327 for pth in sys.path:
328 if not pth:
329 pth = '.'
330 if not os.path.isdir(pth):
331 continue
332 if os.path.exists(os.path.join(pth, 'wx.pth')):
333 base = open(os.path.join(pth, 'wx.pth')).read()
334 return os.path.join(pth, base)
335
336 return None
337
338
d48c1c64
RD
339class _wxPackageInfo(object):
340 def __init__(self, pathname, stripFirst=False):
341 self.pathname = pathname
342 base = os.path.basename(pathname)
343 segments = base.split('-')
344 if stripFirst:
345 segments = segments[1:]
346 self.version = tuple([int(x) for x in segments[0].split('.')])
347 self.options = segments[1:]
348
349
c08df249 350 def Score(self, other, optionsRequired):
d48c1c64 351 score = 0
5029118c 352
0148e434
RD
353 # whatever number of version components given in other must
354 # match exactly
5029118c
RD
355 minlen = min(len(self.version), len(other.version))
356 if self.version[:minlen] != other.version[:minlen]:
357 return 0
0148e434 358 score += 1
c08df249
RD
359
360 # check for matching options, if optionsRequired then the
361 # options are not optional ;-)
d48c1c64
RD
362 for opt in other.options:
363 if opt in self.options:
364 score += 1
c08df249
RD
365 elif optionsRequired:
366 return 0
367
d48c1c64 368 return score
d48c1c64 369
c08df249
RD
370
371 def CheckOptions(self, other, optionsRequired):
372 # if options are not required then this always succeeds
373 if not optionsRequired:
374 return True
375 # otherwise, if we have any option not present in other, then
376 # the match fails.
377 for opt in self.options:
378 if opt not in other.options:
379 return False
380 return True
381
382
54c73383 383
d48c1c64 384 def __lt__(self, other):
54c73383
RD
385 return self.version < other.version or \
386 (self.version == other.version and self.options < other.options)
387 def __le__(self, other):
388 return self.version <= other.version or \
389 (self.version == other.version and self.options <= other.options)
390
d48c1c64 391 def __gt__(self, other):
54c73383
RD
392 return self.version > other.version or \
393 (self.version == other.version and self.options > other.options)
394 def __ge__(self, other):
395 return self.version >= other.version or \
396 (self.version == other.version and self.options >= other.options)
397
d48c1c64 398 def __eq__(self, other):
54c73383 399 return self.version == other.version and self.options == other.options
d48c1c64
RD
400
401
402
5029118c 403#----------------------------------------------------------------------
d48c1c64
RD
404
405if __name__ == '__main__':
17f3e530 406 import pprint
6be3fd57
RD
407
408 #ensureMinimal('2.5')
409 #pprint.pprint(sys.path)
54c73383
RD
410 #sys.exit()
411
412
c08df249 413 def test(version, optionsRequired=False):
4f60dce5 414 # setup
d48c1c64 415 savepath = sys.path[:]
4f60dce5
RD
416
417 #test
c08df249
RD
418 select(version, optionsRequired)
419 print "Asked for %s, (%s):\t got: %s" % (version, optionsRequired, sys.path[0])
420
421 # reset
422 sys.path = savepath[:]
423 global _selected
424 _selected = None
425
426
427 def testEM(version, optionsRequired=False):
428 # setup
429 savepath = sys.path[:]
430
431 #test
432 ensureMinimal(version, optionsRequired)
433 print "EM: Asked for %s, (%s):\t got: %s" % (version, optionsRequired, sys.path[0])
4f60dce5
RD
434
435 # reset
d48c1c64 436 sys.path = savepath[:]
4f60dce5
RD
437 global _selected
438 _selected = None
d48c1c64
RD
439
440
441 # make some test dirs
c08df249
RD
442 names = ['wx-2.4-gtk-ansi',
443 'wx-2.5.2-gtk2-unicode',
444 'wx-2.5.3-gtk-ansi',
445 'wx-2.6-gtk2-unicode',
446 'wx-2.6-gtk2-ansi',
447 'wx-2.6-gtk-ansi',
448 'wx-2.7.1-gtk2-ansi',
449 ]
d48c1c64
RD
450 for name in names:
451 d = os.path.join('/tmp', name)
452 os.mkdir(d)
453 os.mkdir(os.path.join(d, 'wx'))
454
455 # setup sys.path to see those dirs
456 sys.path.append('/tmp')
457
458
459 # now run some tests
5029118c
RD
460 pprint.pprint( getInstalled())
461 print checkInstalled("2.4")
462 print checkInstalled("2.5-unicode")
463 print checkInstalled("2.99-bogus")
6be3fd57
RD
464 print "Current sys.path:"
465 pprint.pprint(sys.path)
466 print
467
d48c1c64
RD
468 test("2.4")
469 test("2.5")
470 test("2.5-gtk2")
471 test("2.5.2")
472 test("2.5-ansi")
473 test("2.5-unicode")
c08df249
RD
474 test("2.6")
475 test("2.6-ansi")
476 test(["2.6-unicode", "2.7-unicode"])
477 test(["2.6", "2.7"])
478 test(["2.6-unicode", "2.7-unicode"], optionsRequired=True)
479
480
d48c1c64
RD
481
482 # There isn't a unicode match for this one, but it will give the best
483 # available 2.4. Should it give an error instead? I don't think so...
484 test("2.4-unicode")
485
4f60dce5 486 # Try asking for multiple versions
c08df249 487 test(["2.5.2", "2.5.3", "2.6"])
4f60dce5 488
d48c1c64
RD
489 try:
490 # expecting an error on this one
c08df249 491 test("2.9")
5029118c 492 except VersionError, e:
c08df249 493 print "Asked for 2.9:\t got Exception:", e
d48c1c64 494
4f60dce5
RD
495 # check for exception when incompatible versions are requested
496 try:
5029118c
RD
497 select("2.4")
498 select("2.5")
499 except VersionError, e:
4f60dce5 500 print "Asked for incompatible versions, got Exception:", e
d48c1c64 501
c08df249
RD
502 _EM_DEBUG=1
503 testEM("2.6")
504 testEM("2.6-unicode")
505 testEM("2.6-unicode", True)
506 try:
507 testEM("2.9")
508 except VersionError, e:
509 print "EM: Asked for 2.9:\t got Exception:", e
510
d48c1c64
RD
511 # cleanup
512 for name in names:
513 d = os.path.join('/tmp', name)
514 os.rmdir(os.path.join(d, 'wx'))
515 os.rmdir(d)
516
517