From af1bca9e22aa7156d50c40873b424caba94fb957 Mon Sep 17 00:00:00 2001 From: "brett.cannon" Date: Mon, 26 Jan 2009 01:16:50 +0000 Subject: [PATCH] Backport importlib in the form of providing importlib.import_module(). This has been done purely to help transitions from 2.7 to 3.1. git-svn-id: http://svn.python.org/projects/python/trunk@68953 6015fed2-1504-0410-9fe1-9d1591cc4771 --- Doc/library/importlib.rst | 27 +++++++ Doc/library/modules.rst | 1 + Lib/importlib.py | 38 ++++++++++ Lib/test/test_importlib.py | 173 +++++++++++++++++++++++++++++++++++++++++++++ Misc/NEWS | 3 + 5 files changed, 242 insertions(+) create mode 100644 Doc/library/importlib.rst create mode 100644 Lib/importlib.py create mode 100644 Lib/test/test_importlib.py diff --git a/Doc/library/importlib.rst b/Doc/library/importlib.rst new file mode 100644 index 0000000000..375c2ae918 --- /dev/null +++ b/Doc/library/importlib.rst @@ -0,0 +1,27 @@ +:mod:`importlib` -- Convenience wrappers for :func:`__import__` +=============================================================== + +.. module:: importlib + :synopsis: Convenience wrappers for __import__ + +.. moduleauthor:: Brett Cannon +.. sectionauthor:: Brett Cannon + +.. versionadded:: 2.7 + +This module is a minor subset of what is available in the more full-featured +package of the same name from Python 3.1 that provides a complete +implementation of :keyword:`import`. What is here has been provided to +help ease in transitioning from 2.7 to 3.1. + + +.. function:: import_module(name, package=None) + + Import a module. The *name* argument specifies what module to + import in absolute or relative terms + (e.g. either ``pkg.mod`` or ``..mod``). If the name is + specified in relative terms, then the *package* argument must be + specified to the package which is to act as the anchor for resolving the + package name (e.g. ``import_module('..mod', 'pkg.subpkg')`` will import + ``pkg.mod``). The specified module will be inserted into + :data:`sys.modules` and returned. diff --git a/Doc/library/modules.rst b/Doc/library/modules.rst index ec6f7cd575..b5543e6139 100644 --- a/Doc/library/modules.rst +++ b/Doc/library/modules.rst @@ -14,6 +14,7 @@ The full list of modules described in this chapter is: .. toctree:: imp.rst + importlib.rst imputil.rst zipimport.rst pkgutil.rst diff --git a/Lib/importlib.py b/Lib/importlib.py new file mode 100644 index 0000000000..89398deda0 --- /dev/null +++ b/Lib/importlib.py @@ -0,0 +1,38 @@ +"""Backport of importlib.import_module from 3.x.""" +import sys + +def _resolve_name(name, package, level): + """Return the absolute name of the module to be imported.""" + level -= 1 + try: + if package.count('.') < level: + raise ValueError("attempted relative import beyond top-level " + "package") + except AttributeError: + raise ValueError("__package__ not set to a string") + base = package.rsplit('.', level)[0] + if name: + return "{0}.{1}".format(base, name) + else: + return base + + +def import_module(name, package=None): + """Import a module. + + The 'package' argument is required when performing a relative import. It + specifies the package to use as the anchor point from which to resolve the + relative import to an absolute import. + + """ + if name.startswith('.'): + if not package: + raise TypeError("relative imports require the 'package' argument") + level = 0 + for character in name: + if character != '.': + break + level += 1 + name = _resolve_name(name[level:], package, level) + __import__(name) + return sys.modules[name] diff --git a/Lib/test/test_importlib.py b/Lib/test/test_importlib.py new file mode 100644 index 0000000000..572b1f169e --- /dev/null +++ b/Lib/test/test_importlib.py @@ -0,0 +1,173 @@ +import contextlib +import imp +import importlib +import sys +import unittest + + +@contextlib.contextmanager +def uncache(*names): + """Uncache a module from sys.modules. + + A basic sanity check is performed to prevent uncaching modules that either + cannot/shouldn't be uncached. + + """ + for name in names: + if name in ('sys', 'marshal', 'imp'): + raise ValueError( + "cannot uncache {0} as it will break _importlib".format(name)) + try: + del sys.modules[name] + except KeyError: + pass + try: + yield + finally: + for name in names: + try: + del sys.modules[name] + except KeyError: + pass + + +@contextlib.contextmanager +def import_state(**kwargs): + """Context manager to manage the various importers and stored state in the + sys module. + + The 'modules' attribute is not supported as the interpreter state stores a + pointer to the dict that the interpreter uses internally; + reassigning to sys.modules does not have the desired effect. + + """ + originals = {} + try: + for attr, default in (('meta_path', []), ('path', []), + ('path_hooks', []), + ('path_importer_cache', {})): + originals[attr] = getattr(sys, attr) + if attr in kwargs: + new_value = kwargs[attr] + del kwargs[attr] + else: + new_value = default + setattr(sys, attr, new_value) + if len(kwargs): + raise ValueError( + 'unrecognized arguments: {0}'.format(kwargs.keys())) + yield + finally: + for attr, value in originals.items(): + setattr(sys, attr, value) + + +class mock_modules(object): + + """A mock importer/loader.""" + + def __init__(self, *names): + self.modules = {} + for name in names: + if not name.endswith('.__init__'): + import_name = name + else: + import_name = name[:-len('.__init__')] + if '.' not in name: + package = None + elif import_name == name: + package = name.rsplit('.', 1)[0] + else: + package = import_name + module = imp.new_module(import_name) + module.__loader__ = self + module.__file__ = '' + module.__package__ = package + module.attr = name + if import_name != name: + module.__path__ = [''] + self.modules[import_name] = module + + def __getitem__(self, name): + return self.modules[name] + + def find_module(self, fullname, path=None): + if fullname not in self.modules: + return None + else: + return self + + def load_module(self, fullname): + if fullname not in self.modules: + raise ImportError + else: + sys.modules[fullname] = self.modules[fullname] + return self.modules[fullname] + + def __enter__(self): + self._uncache = uncache(*self.modules.keys()) + self._uncache.__enter__() + return self + + def __exit__(self, *exc_info): + self._uncache.__exit__(None, None, None) + + + +class ImportModuleTests(unittest.TestCase): + + """Test importlib.import_module.""" + + def test_module_import(self): + # Test importing a top-level module. + with mock_modules('top_level') as mock: + with import_state(meta_path=[mock]): + module = importlib.import_module('top_level') + self.assertEqual(module.__name__, 'top_level') + + def test_absolute_package_import(self): + # Test importing a module from a package with an absolute name. + pkg_name = 'pkg' + pkg_long_name = '{0}.__init__'.format(pkg_name) + name = '{0}.mod'.format(pkg_name) + with mock_modules(pkg_long_name, name) as mock: + with import_state(meta_path=[mock]): + module = importlib.import_module(name) + self.assertEqual(module.__name__, name) + + def test_relative_package_import(self): + # Test importing a module from a package through a relatve import. + pkg_name = 'pkg' + pkg_long_name = '{0}.__init__'.format(pkg_name) + module_name = 'mod' + absolute_name = '{0}.{1}'.format(pkg_name, module_name) + relative_name = '.{0}'.format(module_name) + with mock_modules(pkg_long_name, absolute_name) as mock: + with import_state(meta_path=[mock]): + module = importlib.import_module(relative_name, pkg_name) + self.assertEqual(module.__name__, absolute_name) + + def test_absolute_import_with_package(self): + # Test importing a module from a package with an absolute name with + # the 'package' argument given. + pkg_name = 'pkg' + pkg_long_name = '{0}.__init__'.format(pkg_name) + name = '{0}.mod'.format(pkg_name) + with mock_modules(pkg_long_name, name) as mock: + with import_state(meta_path=[mock]): + module = importlib.import_module(name, pkg_name) + self.assertEqual(module.__name__, name) + + def test_relative_import_wo_package(self): + # Relative imports cannot happen without the 'package' argument being + # set. + self.assertRaises(TypeError, importlib.import_module, '.support') + + +def test_main(): + from test.test_support import run_unittest + run_unittest(ImportModuleTests) + + +if __name__ == '__main__': + test_main() diff --git a/Misc/NEWS b/Misc/NEWS index 9878199e67..da016c1d18 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -145,6 +145,9 @@ Core and Builtins Library ------- +- Backport importlib from Python 3.1. Only the import_module() function has + been backported to help facilitate transitions from 2.7 to 3.1. + - Issue #1885: distutils. When running sdist with --formats=tar,gztar the tar file was overriden by the gztar one. -- 2.11.4.GIT