]> git.saurik.com Git - wxWidgets.git/blob - wxPython/wxversion/wxversion.py
Added optional parameter to wxversion.select and friends that makes
[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 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 _selected = bestMatch
153
154 #----------------------------------------------------------------------
155
156 UPDATE_URL = "http://wxPython.org/"
157 #UPDATE_URL = "http://sourceforge.net/project/showfiles.php?group_id=10718"
158
159 _EM_DEBUG=0
160
161 def ensureMinimal(minVersion, optionsRequired=False):
162 """
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.
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'):
174 raise VersionError("wxversion.ensureMinimal() must be called before wxPython is imported")
175
176 bestMatch = None
177 minv = _wxPackageInfo(minVersion)
178
179 # check the default version first
180 defaultPath = _find_default()
181 if defaultPath:
182 defv = _wxPackageInfo(defaultPath, True)
183 if defv >= minv and minv.CheckOptions(defv, optionsRequired):
184 bestMatch = defv
185
186 # if still no match then check look at all installed versions
187 if bestMatch is None:
188 installed = _find_installed()
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
198 if bestMatch is None:
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
202 import wx, webbrowser
203 versions = "\n".join([" "+ver for ver in getInstalled()])
204 app = wx.PySimpleApp()
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:
213 webbrowser.open(UPDATE_URL)
214 app.MainLoop()
215 sys.exit()
216
217 sys.path.insert(0, bestMatch.pathname)
218 global _selected
219 _selected = bestMatch
220
221
222 #----------------------------------------------------------------------
223
224 def checkInstalled(versions, optionsRequired=False):
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
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`.
234 """
235
236 if type(versions) == str:
237 versions = [versions]
238 installed = _find_installed()
239 bestMatch = _get_best_match(installed, versions, optionsRequired)
240 return bestMatch is not None
241
242 #----------------------------------------------------------------------
243
244 def getInstalled():
245 """
246 Returns a list of strings representing the installed wxPython
247 versions that are found on the system.
248 """
249 installed = _find_installed()
250 return [os.path.basename(p.pathname)[3:] for p in installed]
251
252
253
254 #----------------------------------------------------------------------
255 # private helpers...
256
257 def _get_best_match(installed, versions, optionsRequired):
258 bestMatch = None
259 bestScore = 0
260 for pkg in installed:
261 for ver in versions:
262 score = pkg.Score(_wxPackageInfo(ver), optionsRequired)
263 if score > bestScore:
264 bestMatch = pkg
265 bestScore = score
266 return bestMatch
267
268
269 _pattern = "wx-[0-9].*"
270 def _find_installed(removeExisting=False):
271 installed = []
272 toRemove = []
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
285 # if it's a wx path that's already in the sys.path then mark
286 # it for removal and then skip it
287 if fnmatch.fnmatchcase(base, _pattern):
288 toRemove.append(pth)
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
301 if removeExisting:
302 for rem in toRemove:
303 del sys.path[sys.path.index(rem)]
304
305 installed.sort()
306 installed.reverse()
307 return installed
308
309
310 # Scan the sys.path looking for either a directory matching _pattern,
311 # or a wx.pth file
312 def _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
339 class _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
350 def Score(self, other, optionsRequired):
351 score = 0
352
353 # whatever number of version components given in other must
354 # match exactly
355 minlen = min(len(self.version), len(other.version))
356 if self.version[:minlen] != other.version[:minlen]:
357 return 0
358 score += 1
359
360 # check for matching options, if optionsRequired then the
361 # options are not optional ;-)
362 for opt in other.options:
363 if opt in self.options:
364 score += 1
365 elif optionsRequired:
366 return 0
367
368 return score
369
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
383
384 def __lt__(self, other):
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
391 def __gt__(self, other):
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
398 def __eq__(self, other):
399 return self.version == other.version and self.options == other.options
400
401
402
403 #----------------------------------------------------------------------
404
405 if __name__ == '__main__':
406 import pprint
407
408 #ensureMinimal('2.5')
409 #pprint.pprint(sys.path)
410 #sys.exit()
411
412
413 def test(version, optionsRequired=False):
414 # setup
415 savepath = sys.path[:]
416
417 #test
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])
434
435 # reset
436 sys.path = savepath[:]
437 global _selected
438 _selected = None
439
440
441 # make some test dirs
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 ]
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
460 pprint.pprint( getInstalled())
461 print checkInstalled("2.4")
462 print checkInstalled("2.5-unicode")
463 print checkInstalled("2.99-bogus")
464 print "Current sys.path:"
465 pprint.pprint(sys.path)
466 print
467
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")
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
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
486 # Try asking for multiple versions
487 test(["2.5.2", "2.5.3", "2.6"])
488
489 try:
490 # expecting an error on this one
491 test("2.9")
492 except VersionError, e:
493 print "Asked for 2.9:\t got Exception:", e
494
495 # check for exception when incompatible versions are requested
496 try:
497 select("2.4")
498 select("2.5")
499 except VersionError, e:
500 print "Asked for incompatible versions, got Exception:", e
501
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
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