├── tests ├── versioned-libs │ ├── testlib-1.0 │ │ └── testlib.py │ ├── testlib-2.0 │ │ └── testlib.py │ ├── using_testlib_10.py │ └── using_testlib_20.py └── tests.py ├── example ├── mylib-1.0 │ └── mylib.py ├── mylib-2.0 │ └── mylib.py ├── other_module.py └── example.py ├── setup.py ├── LICENSE ├── README └── multiversion.py /tests/versioned-libs/testlib-1.0/testlib.py: -------------------------------------------------------------------------------- 1 | def a_function(): 2 | return 'from version 1.0' 3 | -------------------------------------------------------------------------------- /tests/versioned-libs/testlib-2.0/testlib.py: -------------------------------------------------------------------------------- 1 | def a_function(): 2 | return 'from version 2.0' 3 | -------------------------------------------------------------------------------- /example/mylib-1.0/mylib.py: -------------------------------------------------------------------------------- 1 | version = '1.0' 2 | 3 | import mylib 4 | print 'self import in 2.0:', mylib 5 | -------------------------------------------------------------------------------- /example/mylib-2.0/mylib.py: -------------------------------------------------------------------------------- 1 | version = '2.0' 2 | 3 | import mylib 4 | print 'self import in 2.0:', mylib 5 | -------------------------------------------------------------------------------- /example/other_module.py: -------------------------------------------------------------------------------- 1 | import multiversion 2 | multiversion.require_version('mylib', '2.0') 3 | 4 | import mylib 5 | print 'mylib in %s: %s' % (__name__, mylib.version) 6 | -------------------------------------------------------------------------------- /tests/versioned-libs/using_testlib_10.py: -------------------------------------------------------------------------------- 1 | import multiversion 2 | multiversion.require_version('testlib', '1.0') 3 | 4 | from testlib import a_function 5 | import testlib as mod 6 | -------------------------------------------------------------------------------- /tests/versioned-libs/using_testlib_20.py: -------------------------------------------------------------------------------- 1 | import multiversion 2 | multiversion.require_version('testlib', '2.0') 3 | 4 | from testlib import a_function 5 | import testlib as mod 6 | -------------------------------------------------------------------------------- /example/example.py: -------------------------------------------------------------------------------- 1 | import multiversion 2 | multiversion.require_version('mylib', '1.0') 3 | 4 | import mylib 5 | print 'mylib in %s: %s' % (__name__, mylib.version) 6 | 7 | import other_module 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | try: 2 | from setuptools import setup 3 | except ImportError: 4 | from distutils.core import setup 5 | 6 | 7 | setup( 8 | name='multiversion', 9 | author='Armin Ronacher', 10 | author_email='armin.ronacher@active-4.com', 11 | version='1.0', 12 | url='http://github.com/mitsuhiko/multiversion', 13 | py_modules=['multiversion'], 14 | description='Allows loading of multiple versions of the same Python module', 15 | long_description=None, 16 | zip_safe=False, 17 | classifiers=[ 18 | 'License :: OSI Approved :: BSD License', 19 | 'Programming Language :: Python' 20 | ] 21 | ) 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 by Armin Ronacher. 2 | 3 | Some rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are 7 | met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above 13 | copyright notice, this list of conditions and the following 14 | disclaimer in the documentation and/or other materials provided 15 | with the distribution. 16 | 17 | * The names of the contributors may not be used to endorse or 18 | promote products derived from this software without specific 19 | prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 24 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 25 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 26 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 27 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 29 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /tests/tests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import unittest 4 | 5 | 6 | here = os.path.dirname(__file__) 7 | sys.path.extend(( 8 | os.path.join(here, os.path.pardir), 9 | os.path.join(here, 'versioned-libs') 10 | )) 11 | 12 | 13 | class SimpleTestCase(unittest.TestCase): 14 | 15 | def test_basic_functionality(self): 16 | import using_testlib_10 as v1 17 | import using_testlib_20 as v2 18 | self.assertEqual(v1.a_function(), 'from version 1.0') 19 | self.assertEqual(v2.a_function(), 'from version 2.0') 20 | 21 | self.assert_('testlib' in sys.modules) 22 | 23 | def test_naming(self): 24 | import multiversion 25 | import using_testlib_10 as v1 26 | import using_testlib_20 as v2 27 | prefix = multiversion.space.__name__ + '.' 28 | self.assertEqual(v1.mod.__name__, prefix + 'testlib___312e30.testlib') 29 | self.assertEqual(v2.mod.__name__, prefix + 'testlib___322e30.testlib') 30 | 31 | def test_proxy(self): 32 | # trigger proxy 33 | import using_testlib_10 as v1 34 | import multiversion 35 | self.assertEqual(v1.a_function(), 'from version 1.0') 36 | 37 | import testlib 38 | try: 39 | testlib.a_function 40 | except AttributeError: 41 | pass 42 | else: 43 | self.fail('failed') 44 | 45 | multiversion.require_version('testlib', '1.0', 46 | globals=globals()) 47 | self.assertEqual(testlib.a_function(), 'from version 1.0') 48 | 49 | 50 | if __name__ == '__main__': 51 | unittest.main() 52 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | 2 | multiversion 3 | ```````````` 4 | 5 | What many (including myself) said could not be done is now working on 6 | any conforming Python 2.x interpreter. How does it work? Have a look 7 | at the example module. 8 | 9 | // Implementation 10 | 11 | The implementation works by rewriting import calls internally into an 12 | version encoded name. That way we can still take advantage of the 13 | internal Python module caching. The modules are found on the whole 14 | PYTHONPATH by looking for ``modulename-version``. If such a folder 15 | exists it will add a fake module and provide whatever module is stored 16 | in there versioned. 17 | 18 | The rewriting is clever enough to track imports inside a module to 19 | itself and its submodules. 20 | 21 | // Limitations 22 | 23 | The boundary is the current file. So you can't have two different 24 | versions of a library to be used by the same file. But you can 25 | separate the code into two files, each of which depends on a different 26 | version of a specific library. 27 | 28 | // Why? 29 | 30 | Because I needed something for my EuroPython talk that involves all 31 | kinds of retarded hackery you really shouldn't be doing. 32 | 33 | // Does it work? 34 | 35 | Surprisingly well actually. 36 | 37 | // Example usage 38 | 39 | The following code will look for Jinja2 in a folder `jinja2-1.0` 40 | somewhere on the PYTHONPATH. This folder will have to contain 41 | another folder called `jinja2` which is the actual package to 42 | be imported then. Instead of a package it can also be a regular 43 | Python module. 44 | 45 | import multiversion 46 | multiversion.require_version('jinja2', '1.0') 47 | 48 | from jinja2 import Template 49 | ... 50 | 51 | -------------------------------------------------------------------------------- /multiversion.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | multiversion 4 | ~~~~~~~~~~~~ 5 | 6 | This implements a hack to support packages in multiple versions running 7 | side by side in Python. It is supported by the language and as such 8 | should work on any conforming Python interpreter. 9 | 10 | The downside is that this bypasses meta hooks so it will only be able 11 | to import regular modules, c extensions and modules can can be found 12 | with the help of a path hook. 13 | 14 | :copyright: (c) Copyright 2011 by Armin Ronacher. 15 | :license: BSD, see LICENSE for more details. 16 | """ 17 | import os 18 | import sys 19 | import imp 20 | import binascii 21 | import weakref 22 | from types import ModuleType 23 | import __builtin__ 24 | 25 | 26 | actual_import = __builtin__.__import__ 27 | 28 | 29 | space = imp.new_module('multiversion.space') 30 | space.__path__ = [] 31 | 32 | sys.modules[space.__name__] = space 33 | 34 | 35 | class ModuleProxy(ModuleType): 36 | """Used to proxy to an actual module. This is needed because many 37 | people do `__import__` + a lookup in sys.modules. 38 | """ 39 | 40 | def __init__(self, name): 41 | ModuleType.__init__(self, name) 42 | 43 | def __getattr__(self, name): 44 | mod = get_actual_module(self.__name__, stacklevel=2) 45 | if mod is None: 46 | raise AttributeError(name) 47 | return getattr(mod, name) 48 | 49 | def __setattr__(self, name, value): 50 | mod = get_actual_module(self.__name__, stacklevel=2) 51 | if mod is None: 52 | raise AttributeError(name) 53 | return setattr(mod, name, value) 54 | 55 | 56 | def get_actual_module(name, stacklevel=1): 57 | """From the caller's view this returns the actual module that was 58 | requested for a given name. 59 | """ 60 | globals = sys._getframe(stacklevel).f_globals 61 | cache_key = get_cache_key(name, globals) 62 | if cache_key is not None: 63 | full_name = '%s.%s' % (get_internal_name(cache_key), name) 64 | return sys.modules[full_name] 65 | 66 | 67 | def require_version(library, version, globals=None): 68 | """Has to be callde at toplevel before importing a module to notify 69 | the multiversion system about the version that should be loaded for 70 | this particular library. 71 | """ 72 | if globals is None: 73 | frm = sys._getframe(1) 74 | if frm.f_globals is not frm.f_locals: 75 | raise RuntimeError('version requirements must happen toplevel') 76 | globals = frm.f_globals 77 | mapping = globals.setdefault('__multiversion_mapping__', {}) 78 | if library in mapping: 79 | raise RuntimeError('requirement already specified') 80 | mapping[library] = version 81 | 82 | 83 | # shortcut :) 84 | require = require_version 85 | 86 | 87 | def version_from_module_name(module): 88 | """Extracts the package and version information from the given internal 89 | module name. If it's not a versioned library it will return `None`. 90 | """ 91 | if not module.startswith(space.__name__ + '.'): 92 | return None 93 | result = module[len(space.__name__) + 1].split('.', 1) 94 | if len(result) == 2 and '___' in result[1]: 95 | return binascii.unhexlify(result[1].rsplit('___', 1)[1]) 96 | 97 | 98 | def get_cache_key(name, globals): 99 | """Returns the cache key for the given module. The globals dictionary 100 | is required for the magic to work. It's used as source for the package 101 | name. The cache key is in the format ``(package, version)``. If the 102 | given import is not versioned it will return `None`. 103 | """ 104 | mapping = globals.get('__multiversion_mapping__') 105 | if mapping is None: 106 | return 107 | package = name.split('.', 1)[0] 108 | version = mapping.get(package) 109 | if version is None: 110 | version = version_from_module_name(globals.get('__name__')) 111 | if version is None: 112 | return 113 | 114 | return package, version 115 | 116 | 117 | def get_internal_name(cache_key): 118 | """Converts a cache key into an internal space module name.""" 119 | package, version = cache_key 120 | return 'multiversion.space.%s___%s' % (package, binascii.hexlify(version)) 121 | 122 | 123 | def rewrite_import_name(name, cache_key): 124 | """Rewrites a whole import line according to the cache key.""" 125 | return '%s.%s' % (get_internal_name(cache_key), name) 126 | 127 | 128 | def version_not_loaded(cache_key): 129 | """Checks if the given module and version was not loaded so far.""" 130 | internal_name = get_internal_name(cache_key) 131 | return sys.modules.get(internal_name) is None 132 | 133 | 134 | def load_version(cache_key): 135 | """Loads a version of a module. Will raise `ImportError` if it fails 136 | doing so. 137 | """ 138 | fs_name = '%s-%s' % cache_key 139 | internal_name = get_internal_name(cache_key) 140 | for path_entry in sys.path: 141 | full_path = os.path.join(path_entry, fs_name) 142 | if not os.path.isdir(full_path): 143 | continue 144 | mod = imp.new_module(internal_name) 145 | setattr(space, internal_name.rsplit('.', 1)[1], mod) 146 | mod.__path__ = [full_path] 147 | sys.modules[mod.__name__] = mod 148 | return 149 | raise ImportError('Version %r of %r not found' % cache_key[::-1]) 150 | 151 | 152 | def version_import(name, globals=None, locals=None, fromlist=None, level=-1): 153 | """An import hook that performs the versioned import. It can't work 154 | on the level of the regular import hooks as it's actually renaming 155 | imports. 156 | """ 157 | if globals is None: 158 | globals = sys._getframe(1).f_globals 159 | if locals is None: 160 | locals = {} 161 | if fromlist is None: 162 | fromlist = [] 163 | key = get_cache_key(name, globals) 164 | actual_name = name 165 | 166 | if key is not None: 167 | actual_name = rewrite_import_name(actual_name, key) 168 | if version_not_loaded(key): 169 | load_version(key) 170 | 171 | if not fromlist: 172 | fromlist = ['__name__'] 173 | rv = actual_import(actual_name, globals, locals, fromlist, level) 174 | proxy = sys.modules.get(name) 175 | if proxy is None: 176 | rv.__multiversion_proxy__ = proxy = ModuleProxy(name) 177 | def cleanup_proxy(ref): 178 | try: 179 | sys.modules.pop(name, None) 180 | except (TypeError, AttributeError): 181 | pass 182 | sys.modules[name] = weakref.proxy(proxy, cleanup_proxy) 183 | return rv 184 | 185 | 186 | __builtin__.__import__ = version_import 187 | --------------------------------------------------------------------------------