]>
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 | |
5029118c | 18 | imported when it does 'import wx'. You use it like this:: |
d48c1c64 RD |
19 | |
20 | import wxversion | |
5029118c RD |
21 | wxversion.select('2.4') |
22 | import wx | |
23 | ||
24 | Or additional build options can also be selected, like this:: | |
25 | ||
26 | import wxversion | |
27 | wxversion.select('2.5.3-unicode') | |
d48c1c64 RD |
28 | import wx |
29 | ||
30 | Of course the default wxPython version can also be controlled by | |
31 | setting PYTHONPATH or by editing the wx.pth path configuration file, | |
32 | but using wxversion will allow an application to manage the version | |
33 | selection itself rather than depend on the user to setup the | |
34 | environment correctly. | |
35 | ||
36 | It works by searching the sys.path for directories matching wx-* and | |
54c73383 | 37 | then comparing them to what was passed to the select function. If a |
d48c1c64 | 38 | match is found then that path is inserted into sys.path. |
5029118c RD |
39 | |
40 | NOTE: If you are making a 'bundle' of your application with a tool | |
41 | like py2exe then you should *not* use the wxversion module since it | |
54c73383 RD |
42 | looks at the filesystem for the directories on sys.path, it will fail |
43 | in a bundled environment. Instead you should simply ensure that the | |
5029118c RD |
44 | version of wxPython that you want is found by default on the sys.path |
45 | when making the bundled version by setting PYTHONPATH. Then that | |
46 | version will be included in your bundle and your app will work as | |
47 | expected. Py2exe and the others usually have a way to tell at runtime | |
48 | if they are running from a bundle or running raw, so you can check | |
49 | that and only use wxversion if needed. For example, for py2exe:: | |
50 | ||
51 | if not hasattr(sys, 'frozen'): | |
52 | import wxversion | |
53 | wxversion.select('2.5') | |
54 | import wx | |
55 | ||
0148e434 RD |
56 | More documentation on wxversion and multi-version installs can be |
57 | found at: http://wiki.wxpython.org/index.cgi/MultiVersionInstalls | |
58 | ||
d48c1c64 RD |
59 | """ |
60 | ||
61 | import sys, os, glob, fnmatch | |
62 | ||
63 | ||
4f60dce5 | 64 | _selected = None |
5029118c | 65 | class VersionError(Exception): |
4f60dce5 | 66 | pass |
d48c1c64 | 67 | |
5029118c | 68 | #---------------------------------------------------------------------- |
d48c1c64 | 69 | |
5029118c | 70 | def select(versions): |
d48c1c64 | 71 | """ |
5029118c RD |
72 | Search for a wxPython installation that matches version. If one |
73 | is found then sys.path is modified so that version will be | |
74 | imported with a 'import wx', otherwise a VersionError exception is | |
75 | raised. This funciton should only be caled once at the begining | |
76 | of the application before wxPython is imported. | |
d48c1c64 | 77 | |
93ba536a RD |
78 | :param version: Specifies the version to look for, it can |
79 | either be a string or a list of strings. Each | |
80 | string is compared to the installed wxPythons | |
81 | and the best match is inserted into the | |
82 | sys.path, allowing an 'import wx' to find that | |
83 | version. | |
d48c1c64 RD |
84 | |
85 | The version string is composed of the dotted | |
86 | version number (at least 2 of the 4 components) | |
87 | optionally followed by hyphen ('-') separated | |
88 | options (wx port, unicode/ansi, flavour, etc.) A | |
89 | match is determined by how much of the installed | |
90 | version matches what is given in the version | |
91 | parameter. If the version number components don't | |
92 | match then the score is zero, otherwise the score | |
93 | is increased for every specified optional component | |
94 | that is specified and that matches. | |
95 | """ | |
d48c1c64 RD |
96 | if type(versions) == str: |
97 | versions = [versions] | |
4f60dce5 RD |
98 | |
99 | global _selected | |
100 | if _selected is not None: | |
101 | # A version was previously selected, ensure that it matches | |
102 | # this new request | |
103 | for ver in versions: | |
104 | if _selected.Score(_wxPackageInfo(ver)) > 0: | |
105 | return | |
106 | # otherwise, raise an exception | |
5029118c | 107 | raise VersionError("A previously selected wx version does not match the new request.") |
4f60dce5 | 108 | |
0148e434 RD |
109 | # If we get here then this is the first time wxversion is used, |
110 | # ensure that wxPython hasn't been imported yet. | |
4f60dce5 | 111 | if sys.modules.has_key('wx') or sys.modules.has_key('wxPython'): |
54c73383 RD |
112 | raise VersionError("wxversion.select() must be called before wxPython is imported") |
113 | ||
4f60dce5 RD |
114 | # Look for a matching version and manipulate the sys.path as |
115 | # needed to allow it to be imported. | |
54c73383 RD |
116 | installed = _find_installed(True) |
117 | bestMatch = _get_best_match(installed, versions) | |
5029118c RD |
118 | |
119 | if bestMatch is None: | |
120 | raise VersionError("Requested version of wxPython not found") | |
121 | ||
122 | sys.path.insert(0, bestMatch.pathname) | |
123 | _selected = bestMatch | |
124 | ||
125 | #---------------------------------------------------------------------- | |
126 | ||
5bb05e6d RD |
127 | UPDATE_URL = "http://wxPython.org/" |
128 | #UPDATE_URL = "http://sourceforge.net/project/showfiles.php?group_id=10718" | |
129 | ||
6be3fd57 RD |
130 | |
131 | def ensureMinimal(minVersion): | |
54c73383 | 132 | """ |
6be3fd57 RD |
133 | Checks to see if the default version of wxPython is greater-than |
134 | or equal to `minVersion`. If not then it will try to find an | |
135 | installed version that is >= minVersion. If none are available | |
136 | then a message is displayed that will inform the user and will | |
137 | offer to open their web browser to the wxPython downloads page, | |
138 | and will then exit the application. | |
54c73383 RD |
139 | """ |
140 | assert type(minVersion) == str | |
141 | ||
142 | # ensure that wxPython hasn't been imported yet. | |
143 | if sys.modules.has_key('wx') or sys.modules.has_key('wxPython'): | |
6be3fd57 RD |
144 | raise VersionError("wxversion.ensureMinimal() must be called before wxPython is imported") |
145 | ||
54c73383 | 146 | bestMatch = None |
54c73383 | 147 | minv = _wxPackageInfo(minVersion) |
6be3fd57 RD |
148 | defaultPath = _find_default() |
149 | if defaultPath: | |
150 | defv = _wxPackageInfo(defaultPath, True) | |
151 | if defv >= minv: | |
152 | bestMatch = defv | |
54c73383 | 153 | |
6be3fd57 RD |
154 | if bestMatch is None: |
155 | installed = _find_installed() | |
156 | if installed: | |
157 | # The list is in reverse sorted order, so if the first one is | |
158 | # big enough then choose it | |
159 | if installed[0] >= minv: | |
160 | bestMatch = installed[0] | |
54c73383 RD |
161 | |
162 | if bestMatch is None: | |
163 | import wx, webbrowser | |
164 | versions = "\n".join([" "+ver for ver in getInstalled()]) | |
165 | app = wx.PySimpleApp() | |
6be3fd57 RD |
166 | result = wx.MessageBox("This application requires a version of wxPython " |
167 | "greater than or equal to %s, but a matching version " | |
168 | "was not found.\n\n" | |
169 | "You currently have these version(s) installed:\n%s\n\n" | |
170 | "Would you like to download a new version of wxPython?\n" | |
171 | % (minVersion, versions), | |
172 | "wxPython Upgrade Needed", style=wx.YES_NO) | |
173 | if result == wx.YES: | |
5bb05e6d | 174 | webbrowser.open(UPDATE_URL) |
54c73383 | 175 | app.MainLoop() |
54c73383 RD |
176 | sys.exit() |
177 | ||
178 | sys.path.insert(0, bestMatch.pathname) | |
179 | _selected = bestMatch | |
180 | ||
181 | ||
182 | #---------------------------------------------------------------------- | |
183 | ||
5029118c RD |
184 | def checkInstalled(versions): |
185 | """ | |
186 | Check if there is a version of wxPython installed that matches one | |
187 | of the versions given. Returns True if so, False if not. This | |
188 | can be used to determine if calling `select` will succeed or not. | |
189 | ||
190 | :param version: Same as in `select`, either a string or a list | |
191 | of strings specifying the version(s) to check | |
192 | for. | |
193 | """ | |
194 | ||
195 | if type(versions) == str: | |
196 | versions = [versions] | |
54c73383 RD |
197 | installed = _find_installed() |
198 | bestMatch = _get_best_match(installed, versions) | |
5029118c RD |
199 | return bestMatch is not None |
200 | ||
201 | #---------------------------------------------------------------------- | |
202 | ||
203 | def getInstalled(): | |
204 | """ | |
205 | Returns a list of strings representing the installed wxPython | |
206 | versions that are found on the system. | |
207 | """ | |
54c73383 RD |
208 | installed = _find_installed() |
209 | return [os.path.basename(p.pathname)[3:] for p in installed] | |
5029118c RD |
210 | |
211 | ||
212 | ||
213 | #---------------------------------------------------------------------- | |
214 | # private helpers... | |
215 | ||
54c73383 | 216 | def _get_best_match(installed, versions): |
5029118c RD |
217 | bestMatch = None |
218 | bestScore = 0 | |
54c73383 | 219 | for pkg in installed: |
d48c1c64 RD |
220 | for ver in versions: |
221 | score = pkg.Score(_wxPackageInfo(ver)) | |
222 | if score > bestScore: | |
223 | bestMatch = pkg | |
224 | bestScore = score | |
5029118c | 225 | return bestMatch |
d48c1c64 RD |
226 | |
227 | ||
228 | _pattern = "wx-[0-9].*" | |
5029118c | 229 | def _find_installed(removeExisting=False): |
d48c1c64 | 230 | installed = [] |
17f3e530 | 231 | toRemove = [] |
d48c1c64 RD |
232 | for pth in sys.path: |
233 | ||
234 | # empty means to look in the current dir | |
235 | if not pth: | |
236 | pth = '.' | |
237 | ||
238 | # skip it if it's not a package dir | |
239 | if not os.path.isdir(pth): | |
240 | continue | |
241 | ||
242 | base = os.path.basename(pth) | |
243 | ||
17f3e530 RD |
244 | # if it's a wx path that's already in the sys.path then mark |
245 | # it for removal and then skip it | |
d48c1c64 | 246 | if fnmatch.fnmatchcase(base, _pattern): |
17f3e530 | 247 | toRemove.append(pth) |
d48c1c64 RD |
248 | continue |
249 | ||
250 | # now look in the dir for matching subdirs | |
251 | for name in glob.glob(os.path.join(pth, _pattern)): | |
252 | # make sure it's a directory | |
253 | if not os.path.isdir(name): | |
254 | continue | |
255 | # and has a wx subdir | |
256 | if not os.path.exists(os.path.join(name, 'wx')): | |
257 | continue | |
258 | installed.append(_wxPackageInfo(name, True)) | |
259 | ||
5029118c RD |
260 | if removeExisting: |
261 | for rem in toRemove: | |
262 | del sys.path[sys.path.index(rem)] | |
17f3e530 | 263 | |
d48c1c64 RD |
264 | installed.sort() |
265 | installed.reverse() | |
266 | return installed | |
267 | ||
268 | ||
6be3fd57 RD |
269 | # Scan the sys.path looking for either a directory matching _pattern, |
270 | # or a wx.pth file | |
271 | def _find_default(): | |
272 | for pth in sys.path: | |
273 | # empty means to look in the current dir | |
274 | if not pth: | |
275 | pth = '.' | |
276 | ||
277 | # skip it if it's not a package dir | |
278 | if not os.path.isdir(pth): | |
279 | continue | |
280 | ||
281 | # does it match the pattern? | |
282 | base = os.path.basename(pth) | |
283 | if fnmatch.fnmatchcase(base, _pattern): | |
284 | return pth | |
285 | ||
286 | for pth in sys.path: | |
287 | if not pth: | |
288 | pth = '.' | |
289 | if not os.path.isdir(pth): | |
290 | continue | |
291 | if os.path.exists(os.path.join(pth, 'wx.pth')): | |
292 | base = open(os.path.join(pth, 'wx.pth')).read() | |
293 | return os.path.join(pth, base) | |
294 | ||
295 | return None | |
296 | ||
297 | ||
d48c1c64 RD |
298 | class _wxPackageInfo(object): |
299 | def __init__(self, pathname, stripFirst=False): | |
300 | self.pathname = pathname | |
301 | base = os.path.basename(pathname) | |
302 | segments = base.split('-') | |
303 | if stripFirst: | |
304 | segments = segments[1:] | |
305 | self.version = tuple([int(x) for x in segments[0].split('.')]) | |
306 | self.options = segments[1:] | |
307 | ||
308 | ||
309 | def Score(self, other): | |
310 | score = 0 | |
5029118c | 311 | |
0148e434 RD |
312 | # whatever number of version components given in other must |
313 | # match exactly | |
5029118c RD |
314 | minlen = min(len(self.version), len(other.version)) |
315 | if self.version[:minlen] != other.version[:minlen]: | |
316 | return 0 | |
0148e434 | 317 | score += 1 |
5029118c | 318 | |
d48c1c64 RD |
319 | for opt in other.options: |
320 | if opt in self.options: | |
321 | score += 1 | |
322 | return score | |
323 | ||
324 | ||
54c73383 | 325 | |
d48c1c64 | 326 | def __lt__(self, other): |
54c73383 RD |
327 | return self.version < other.version or \ |
328 | (self.version == other.version and self.options < other.options) | |
329 | def __le__(self, other): | |
330 | return self.version <= other.version or \ | |
331 | (self.version == other.version and self.options <= other.options) | |
332 | ||
d48c1c64 | 333 | def __gt__(self, other): |
54c73383 RD |
334 | return self.version > other.version or \ |
335 | (self.version == other.version and self.options > other.options) | |
336 | def __ge__(self, other): | |
337 | return self.version >= other.version or \ | |
338 | (self.version == other.version and self.options >= other.options) | |
339 | ||
d48c1c64 | 340 | def __eq__(self, other): |
54c73383 | 341 | return self.version == other.version and self.options == other.options |
d48c1c64 RD |
342 | |
343 | ||
344 | ||
5029118c | 345 | #---------------------------------------------------------------------- |
d48c1c64 RD |
346 | |
347 | if __name__ == '__main__': | |
17f3e530 | 348 | import pprint |
6be3fd57 RD |
349 | |
350 | #ensureMinimal('2.5') | |
351 | #pprint.pprint(sys.path) | |
54c73383 RD |
352 | #sys.exit() |
353 | ||
354 | ||
d48c1c64 | 355 | def test(version): |
4f60dce5 | 356 | # setup |
d48c1c64 | 357 | savepath = sys.path[:] |
4f60dce5 RD |
358 | |
359 | #test | |
5029118c | 360 | select(version) |
d48c1c64 | 361 | print "Asked for %s:\t got: %s" % (version, sys.path[0]) |
6be3fd57 RD |
362 | pprint.pprint(sys.path) |
363 | ||
4f60dce5 RD |
364 | |
365 | # reset | |
d48c1c64 | 366 | sys.path = savepath[:] |
4f60dce5 RD |
367 | global _selected |
368 | _selected = None | |
d48c1c64 RD |
369 | |
370 | ||
371 | # make some test dirs | |
372 | names = ['wx-2.4', | |
373 | 'wx-2.5.2', | |
374 | 'wx-2.5.2.9-gtk2-unicode', | |
375 | 'wx-2.5.2.9-gtk-ansi', | |
376 | 'wx-2.5.1', | |
377 | 'wx-2.5.2.8-gtk2-unicode', | |
378 | 'wx-2.5.3'] | |
379 | for name in names: | |
380 | d = os.path.join('/tmp', name) | |
381 | os.mkdir(d) | |
382 | os.mkdir(os.path.join(d, 'wx')) | |
383 | ||
384 | # setup sys.path to see those dirs | |
385 | sys.path.append('/tmp') | |
386 | ||
387 | ||
388 | # now run some tests | |
5029118c RD |
389 | pprint.pprint( getInstalled()) |
390 | print checkInstalled("2.4") | |
391 | print checkInstalled("2.5-unicode") | |
392 | print checkInstalled("2.99-bogus") | |
6be3fd57 RD |
393 | print "Current sys.path:" |
394 | pprint.pprint(sys.path) | |
395 | ||
396 | ||
d48c1c64 RD |
397 | test("2.4") |
398 | test("2.5") | |
399 | test("2.5-gtk2") | |
400 | test("2.5.2") | |
401 | test("2.5-ansi") | |
402 | test("2.5-unicode") | |
403 | ||
404 | # There isn't a unicode match for this one, but it will give the best | |
405 | # available 2.4. Should it give an error instead? I don't think so... | |
406 | test("2.4-unicode") | |
407 | ||
4f60dce5 RD |
408 | # Try asking for multiple versions |
409 | test(["2.6", "2.5.3", "2.5.2-gtk2"]) | |
410 | ||
d48c1c64 RD |
411 | try: |
412 | # expecting an error on this one | |
413 | test("2.6") | |
5029118c | 414 | except VersionError, e: |
4f60dce5 | 415 | print "Asked for 2.6:\t got Exception:", e |
d48c1c64 | 416 | |
4f60dce5 RD |
417 | # check for exception when incompatible versions are requested |
418 | try: | |
5029118c RD |
419 | select("2.4") |
420 | select("2.5") | |
421 | except VersionError, e: | |
4f60dce5 | 422 | print "Asked for incompatible versions, got Exception:", e |
d48c1c64 RD |
423 | |
424 | # cleanup | |
425 | for name in names: | |
426 | d = os.path.join('/tmp', name) | |
427 | os.rmdir(os.path.join(d, 'wx')) | |
428 | os.rmdir(d) | |
429 | ||
430 |