Skip to content

Commit 7a0b88c

Browse files
author
brett.cannon
committed
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
1 parent 843308e commit 7a0b88c

5 files changed

Lines changed: 242 additions & 0 deletions

File tree

Doc/library/importlib.rst

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
:mod:`importlib` -- Convenience wrappers for :func:`__import__`
2+
===============================================================
3+
4+
.. module:: importlib
5+
:synopsis: Convenience wrappers for __import__
6+
7+
.. moduleauthor:: Brett Cannon <brett@python.org>
8+
.. sectionauthor:: Brett Cannon <brett@python.org>
9+
10+
.. versionadded:: 2.7
11+
12+
This module is a minor subset of what is available in the more full-featured
13+
package of the same name from Python 3.1 that provides a complete
14+
implementation of :keyword:`import`. What is here has been provided to
15+
help ease in transitioning from 2.7 to 3.1.
16+
17+
18+
.. function:: import_module(name, package=None)
19+
20+
Import a module. The *name* argument specifies what module to
21+
import in absolute or relative terms
22+
(e.g. either ``pkg.mod`` or ``..mod``). If the name is
23+
specified in relative terms, then the *package* argument must be
24+
specified to the package which is to act as the anchor for resolving the
25+
package name (e.g. ``import_module('..mod', 'pkg.subpkg')`` will import
26+
``pkg.mod``). The specified module will be inserted into
27+
:data:`sys.modules` and returned.

Doc/library/modules.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ The full list of modules described in this chapter is:
1414
.. toctree::
1515

1616
imp.rst
17+
importlib.rst
1718
imputil.rst
1819
zipimport.rst
1920
pkgutil.rst

Lib/importlib.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""Backport of importlib.import_module from 3.x."""
2+
import sys
3+
4+
def _resolve_name(name, package, level):
5+
"""Return the absolute name of the module to be imported."""
6+
level -= 1
7+
try:
8+
if package.count('.') < level:
9+
raise ValueError("attempted relative import beyond top-level "
10+
"package")
11+
except AttributeError:
12+
raise ValueError("__package__ not set to a string")
13+
base = package.rsplit('.', level)[0]
14+
if name:
15+
return "{0}.{1}".format(base, name)
16+
else:
17+
return base
18+
19+
20+
def import_module(name, package=None):
21+
"""Import a module.
22+
23+
The 'package' argument is required when performing a relative import. It
24+
specifies the package to use as the anchor point from which to resolve the
25+
relative import to an absolute import.
26+
27+
"""
28+
if name.startswith('.'):
29+
if not package:
30+
raise TypeError("relative imports require the 'package' argument")
31+
level = 0
32+
for character in name:
33+
if character != '.':
34+
break
35+
level += 1
36+
name = _resolve_name(name[level:], package, level)
37+
__import__(name)
38+
return sys.modules[name]

Lib/test/test_importlib.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import contextlib
2+
import imp
3+
import importlib
4+
import sys
5+
import unittest
6+
7+
8+
@contextlib.contextmanager
9+
def uncache(*names):
10+
"""Uncache a module from sys.modules.
11+
12+
A basic sanity check is performed to prevent uncaching modules that either
13+
cannot/shouldn't be uncached.
14+
15+
"""
16+
for name in names:
17+
if name in ('sys', 'marshal', 'imp'):
18+
raise ValueError(
19+
"cannot uncache {0} as it will break _importlib".format(name))
20+
try:
21+
del sys.modules[name]
22+
except KeyError:
23+
pass
24+
try:
25+
yield
26+
finally:
27+
for name in names:
28+
try:
29+
del sys.modules[name]
30+
except KeyError:
31+
pass
32+
33+
34+
@contextlib.contextmanager
35+
def import_state(**kwargs):
36+
"""Context manager to manage the various importers and stored state in the
37+
sys module.
38+
39+
The 'modules' attribute is not supported as the interpreter state stores a
40+
pointer to the dict that the interpreter uses internally;
41+
reassigning to sys.modules does not have the desired effect.
42+
43+
"""
44+
originals = {}
45+
try:
46+
for attr, default in (('meta_path', []), ('path', []),
47+
('path_hooks', []),
48+
('path_importer_cache', {})):
49+
originals[attr] = getattr(sys, attr)
50+
if attr in kwargs:
51+
new_value = kwargs[attr]
52+
del kwargs[attr]
53+
else:
54+
new_value = default
55+
setattr(sys, attr, new_value)
56+
if len(kwargs):
57+
raise ValueError(
58+
'unrecognized arguments: {0}'.format(kwargs.keys()))
59+
yield
60+
finally:
61+
for attr, value in originals.items():
62+
setattr(sys, attr, value)
63+
64+
65+
class mock_modules(object):
66+
67+
"""A mock importer/loader."""
68+
69+
def __init__(self, *names):
70+
self.modules = {}
71+
for name in names:
72+
if not name.endswith('.__init__'):
73+
import_name = name
74+
else:
75+
import_name = name[:-len('.__init__')]
76+
if '.' not in name:
77+
package = None
78+
elif import_name == name:
79+
package = name.rsplit('.', 1)[0]
80+
else:
81+
package = import_name
82+
module = imp.new_module(import_name)
83+
module.__loader__ = self
84+
module.__file__ = '<mock __file__>'
85+
module.__package__ = package
86+
module.attr = name
87+
if import_name != name:
88+
module.__path__ = ['<mock __path__>']
89+
self.modules[import_name] = module
90+
91+
def __getitem__(self, name):
92+
return self.modules[name]
93+
94+
def find_module(self, fullname, path=None):
95+
if fullname not in self.modules:
96+
return None
97+
else:
98+
return self
99+
100+
def load_module(self, fullname):
101+
if fullname not in self.modules:
102+
raise ImportError
103+
else:
104+
sys.modules[fullname] = self.modules[fullname]
105+
return self.modules[fullname]
106+
107+
def __enter__(self):
108+
self._uncache = uncache(*self.modules.keys())
109+
self._uncache.__enter__()
110+
return self
111+
112+
def __exit__(self, *exc_info):
113+
self._uncache.__exit__(None, None, None)
114+
115+
116+
117+
class ImportModuleTests(unittest.TestCase):
118+
119+
"""Test importlib.import_module."""
120+
121+
def test_module_import(self):
122+
# Test importing a top-level module.
123+
with mock_modules('top_level') as mock:
124+
with import_state(meta_path=[mock]):
125+
module = importlib.import_module('top_level')
126+
self.assertEqual(module.__name__, 'top_level')
127+
128+
def test_absolute_package_import(self):
129+
# Test importing a module from a package with an absolute name.
130+
pkg_name = 'pkg'
131+
pkg_long_name = '{0}.__init__'.format(pkg_name)
132+
name = '{0}.mod'.format(pkg_name)
133+
with mock_modules(pkg_long_name, name) as mock:
134+
with import_state(meta_path=[mock]):
135+
module = importlib.import_module(name)
136+
self.assertEqual(module.__name__, name)
137+
138+
def test_relative_package_import(self):
139+
# Test importing a module from a package through a relatve import.
140+
pkg_name = 'pkg'
141+
pkg_long_name = '{0}.__init__'.format(pkg_name)
142+
module_name = 'mod'
143+
absolute_name = '{0}.{1}'.format(pkg_name, module_name)
144+
relative_name = '.{0}'.format(module_name)
145+
with mock_modules(pkg_long_name, absolute_name) as mock:
146+
with import_state(meta_path=[mock]):
147+
module = importlib.import_module(relative_name, pkg_name)
148+
self.assertEqual(module.__name__, absolute_name)
149+
150+
def test_absolute_import_with_package(self):
151+
# Test importing a module from a package with an absolute name with
152+
# the 'package' argument given.
153+
pkg_name = 'pkg'
154+
pkg_long_name = '{0}.__init__'.format(pkg_name)
155+
name = '{0}.mod'.format(pkg_name)
156+
with mock_modules(pkg_long_name, name) as mock:
157+
with import_state(meta_path=[mock]):
158+
module = importlib.import_module(name, pkg_name)
159+
self.assertEqual(module.__name__, name)
160+
161+
def test_relative_import_wo_package(self):
162+
# Relative imports cannot happen without the 'package' argument being
163+
# set.
164+
self.assertRaises(TypeError, importlib.import_module, '.support')
165+
166+
167+
def test_main():
168+
from test.test_support import run_unittest
169+
run_unittest(ImportModuleTests)
170+
171+
172+
if __name__ == '__main__':
173+
test_main()

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,9 @@ Core and Builtins
145145
Library
146146
-------
147147

148+
- Backport importlib from Python 3.1. Only the import_module() function has
149+
been backported to help facilitate transitions from 2.7 to 3.1.
150+
148151
- Issue #1885: distutils. When running sdist with --formats=tar,gztar
149152
the tar file was overriden by the gztar one.
150153

0 commit comments

Comments
 (0)