What is VersionSelection?
VersionSelection is the process whereby the desired version of [wxPython] will be used out of multiple versions installed There are a number of proposals as to how to do this. One possible solution is included below. Some involve changes to the [www.python.org Python] language, but this page (for now) just deals with what will work at the moment. For almost all of these, you have to install wx into non-standard locations, so that is not currently listed as a Disadvantage.
Requirements
Basic Goals:
- Multiple versions of wxPython being installed at the same time
- Two or more programs, each using a different version could run at once.
- Easy for developers - for testing with multiple versions etc
- Easy for users - their apps detect the right version to run with
- Cross-Platform solution
More detail:
- The ability for the programmer to specify the version used in the code, ideally only on (or before) the first import of wxPython
- A default version that gets imported with "import wx", just like it's always been
- The ability to specify the version wanted with an environment variable, so that multiple versions could be easily tested
- The ability to specify multiple versions that all work, in a ordered list of some sort.
- The ability to specify a "minimal" version, and higher versions could be used.
- The ability for "from wx.lib import Something" to work as well, respecting the version
Techniques
.pth files
Change the path inside a .pth file.
Advantages:
- Fairly simple
- Cross-platform
Disadvantages:
- Requires change to filesystem to switch version - difficult to set app-specific prefs
- May be difficult for users
- No intelligent version detection
import as
Install into wxNNN and then say:
import wx252 as wx
Advantages:
- No changes to filesystem to switch version
- Cross-platform
Disadvantages:
- Difficult to run same app with different versions
- Developer may specify different version to what user has installed
- No intelligent version detection
symbolic links
Create a symbolic link to the version you want to use
Advantages:
- No special Python code required
Disadvantages:
- Not cross-platform
- Difficult to run different apps with different versions
- Requires change to filesystem to switch version - difficult to set app-specific prefs
- May be difficult for users
- No intelligent version detection
environment variables
This involves having an environment variable set to indicate the desired version A module can then be loaded, either automatically (using sitecustomize) or manually, and the desired version added to the path and imported
Advantages:
- Reasonably cross-platform
- Can easily specify default versions per app, user, etc
- Requires no change to filesystem to switch version
- Since it is Python code, intelligent version detection can be done
Disadvantages:
Need a separate module loaded to determine the version & alter the path
- Needs special setup to locate version based on path
version number in path
This means that wx would be installed into wxPython-2.5.1.5 or wxPython-24 (depending on the scheme chosen). This could be combined with some of the above methods
Issues:
- How to number the directories
- Should the whole version number be included?
A Solution
This is one approach which combines some of the above ideas. Basically each version of wx is installed into a different directory, e.g. wxPython-2.5.1.5 (including the full version number). These directories don't have to be in any particular location. A path file is created for each wx directory and placed on the Python path. This means that without version selection, a single wx version will work as normal, and with multiple versions, one of them will be chosen (whichever ends up coming first in the Python path).
A module (currently called wxselect) reads all the path files and is able to select a version of wx and edit sys.path so that that version is seen by other programs. This selection can be done either by setting environment variables, or by calling a function in wxselect from the main program. One approach I have found helpful is to import wxselect from site-customize.py which means it is always loaded.
Environment variables used are WXPYTHON_PATH (which sets the path of the desired version directly), WXPYTHON_VERSION (chooses the latest version starting with this string, so 2.4 could select 2.4.2.4, 2.5.1 could select 2.5.1.5 etc), WXPYTHON_MINVERSION (chooses the latest version greater than this string, so 2.4 would select 2.5.1.5 if that was the latest available, 2.5 would cause an error if 2.4.2.4 was the latest version).
Choosing the version from the source code of the main application can be done as follows:
import wxselect wxselect.wxVersionFinder.setpath(version="2.5") import wx
setpath takes parameters corresponding to the environment variables - path for WXPYTHONPATH, version for WXPYTHON_VERSION, minversion for WXPYTHON_MINVERSION
Source code
Here is the full source code for wxselect.py
import os import sys import glob """wxselect selects an appropriate wxPython module and adds it to sys.path Version selection is implemented with environment variables" Order of precedence is: WXPYTHON_PATH WXPYTHON_VERSION (looks for a version starting with this - 2.4 or 2.4.2.4 are valid) WXPYTHON_MINVERSION (requires at least this version) Otherwise the latest available version is used """ class EnvConfig: """reads environment variables of the form MODULE_KEY and stores them as self.key""" def __init__(self, modulename, keys): for key in keys: setattr(self, key, os.getenv("%s_%s" % (modulename.upper(), key.upper()))) class VersionFinder: """Finds Versions of a module using module-x.y.z directory names and selects best match for environment variables""" keys = ("minversion", "version", "path", "pythonpath") def __init__(self, modulename, versionimportlist = None, versionattrlist = ["ver", "version", "VERSION", "VERSION_STRING"]): """construct a VersionFinder for the given modulename""" self.modulename = modulename if versionimportlist: self.versionimportlist = versionimportlist else: self.versionimportlist = [os.path.join(self.modulename, "__version__.py")] self.versionattrlist = versionattrlist self.findversions() def findversions(self): """finds all versions of this module by looking at module-x.y.z directories in the Python Path""" self.versions = {} for path in sys.path: filenames = glob.glob(os.path.join(path, '%s-*' % self.modulename)) for filename in filenames: if os.path.isfile(filename) and filename.lower().endswith(os.extsep + "pth"): versionname = os.path.splitext(os.path.basename(filename))[0] versionpaths = [] for versiondir in open(filename): versionpaths.extend(self.readversionpath(versiondir.strip())) elif os.path.isdir(filename): versionname = os.path.basename(filename) versionpaths = self.readversionpath(filename) else: continue version = versionname[len("%s-" % self.modulename):] if version not in self.versions: self.versions[version] = versionpaths return self.versions def readversionpath(self, versiondir): """reads any .pth files in the versiondir and returns the path required for the version""" versionpaths = [versiondir] versionpthfiles = glob.glob(os.path.join(versiondir, '*.pth')) for pthfile in versionpthfiles: for line in open(pthfile, "r"): versionpath = line.strip() if not versionpath: continue if not os.path.isabs(versionpath): versionpath = os.path.join(os.path.dirname(versiondir), versionpath) versionpaths.append(versionpath) return versionpaths def readpathversion(self, versionpath): """reads the module version from the given path""" import imp for versionimportpath in self.versionimportlist: versionfilename = os.path.join(versionpath, versionimportpath) if os.path.isfile(versionfilename): versionmodule = imp.load_source(os.path.basename(versionfilename), versionfilename, open(versionfilename, 'r')) if versionmodule is not None: for versionattrname in self.versionattrlist: version = getattr(versionmodule, versionattrname, None) if version is not None: return version return None def getversionpath(self, version): """looks up the pathsep-joined path for the given version""" return os.path.pathsep.join(self.versions[version]) def listversions(self): """lists known versions""" return self.versions.keys() def getbestversion(self, possibleversions): """finds the best version out of the possibilities""" if possibleversions: return max(possibleversions) def getconfig(self, path=None, version=None, minversion=None): """reads the environment variables and intelligently chooses version and path""" config = EnvConfig(self.modulename, self.keys) if path: config.path = path if version: config.version = version if minversion: config.minversion = minversion if config.path: config.version = self.readpathversion(config.path) else: if config.version: possibleversions = [version for version in self.listversions() if version.startswith(config.version)] elif config.minversion: possibleversions = [version for version in self.listversions() if version >= config.minversion] else: possibleversions = self.listversions() config.version = self.getbestversion(possibleversions) if config.version: config.path = self.getversionpath(config.version) return config def setpath(self, path=None, version=None, minversion=None): """removes other versions from the path and appends the selected path""" allpaths = [] map(allpaths.extend, self.versions.values()) self.removefrompath(allpaths) config = self.getconfig(path, version, minversion) self.appendtopath(config.path) def appendtopath(self, paths): """takes a pathsep-separated path list and adds elements to the Python path at the end""" if paths: pathlist = paths.split(os.path.pathsep) pathlist = [path for path in pathlist if path and os.path.isdir(path)] sys.path.extend(pathlist) def prependtopath(self, paths): """takes a pathsep-separated path list and adds elements to the Python path at the beginning""" if paths: pathlist = paths.split(os.path.pathsep) pathlist = [path for path in pathlist if path and os.path.isdir(path)] sys.path = pathlist + sys.path def removefrompath(self, pathlist): """removes all known versions from the PythonPath""" def normalize(path): return os.path.normcase(os.path.normpath(os.path.abspath(path))) if pathlist: pathlist = [normalize(path) for path in pathlist if path and os.path.isdir(path)] sys.path = [path for path in sys.path if normalize(path) not in pathlist] wx25versionfile = os.path.join("wx", "__version__.py") wx25versionattr = "VERSION_STRING" wx24versionfile = os.path.join("wxPython", "__version__.py") wx24versionattr = "wxVERSION_STRING" wxversionimportlist = [wx25versionfile, wx24versionfile] wxversionattrlist = [wx25versionattr, wx24versionattr] wxVersionFinder = VersionFinder("wxPython", versionimportlist = wxversionimportlist, versionattrlist = wxversionattrlist) if __name__ == "__main__": print "wxPython version selector" print "available versions:" for version, path in wxVersionFinder.versions.iteritems(): print "%s: %s" % (version, path) print config = wxVersionFinder.getconfig() print "selected: %s in %s" % (config.version, config.path) else: wxVersionFinder.setpath() # wxVersionFinder.appendtopath(wxVersionFinder.getconfig().path)