]>
Commit | Line | Data |
---|---|---|
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 | """ | |
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 | |
c08df249 RD |
18 | imported when it does 'import wx'. The main function of this module |
19 | is `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 |
25 | Or additional build options can also be selected, although they will |
26 | not 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 |
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 | ||
d48c1c64 RD |
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 | |
54c73383 | 53 | then comparing them to what was passed to the select function. If a |
d48c1c64 | 54 | match is found then that path is inserted into sys.path. |
5029118c RD |
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 | |
54c73383 RD |
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 | |
5029118c RD |
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 | ||
0148e434 RD |
72 | More documentation on wxversion and multi-version installs can be |
73 | found at: http://wiki.wxpython.org/index.cgi/MultiVersionInstalls | |
74 | ||
d48c1c64 RD |
75 | """ |
76 | ||
a9f3f5a6 | 77 | import re, sys, os, glob, fnmatch |
d48c1c64 RD |
78 | |
79 | ||
4f60dce5 | 80 | _selected = None |
5029118c | 81 | class VersionError(Exception): |
4f60dce5 | 82 | pass |
d48c1c64 | 83 | |
5029118c | 84 | #---------------------------------------------------------------------- |
d48c1c64 | 85 | |
c08df249 | 86 | def 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 |
160 | UPDATE_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 | 165 | def 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 | 232 | def 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 | ||
252 | def 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 | 265 | def _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 | 278 | def _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 | |
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 | ||
d48c1c64 RD |
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 | ||
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 | |
413 | if __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 | ||
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 |