]> granicus.if.org Git - python/commitdiff
Add initial implementation of importlib. See the NOTES files for what is
authorBrett Cannon <bcannon@gmail.com>
Sun, 18 Jan 2009 00:24:28 +0000 (00:24 +0000)
committerBrett Cannon <bcannon@gmail.com>
Sun, 18 Jan 2009 00:24:28 +0000 (00:24 +0000)
planned for the package.

There are no docs yet, but they are coming once the API for the first new
function, importlib.import_module() is finalized.

35 files changed:
Lib/importlib/NOTES [new file with mode: 0644]
Lib/importlib/__init__.py [new file with mode: 0644]
Lib/importlib/_bootstrap.py [new file with mode: 0644]
Lib/importlib/test/__init__.py [new file with mode: 0644]
Lib/importlib/test/builtin/__init__.py [new file with mode: 0644]
Lib/importlib/test/builtin/test_finder.py [new file with mode: 0644]
Lib/importlib/test/builtin/test_loader.py [new file with mode: 0644]
Lib/importlib/test/extension/__init__.py [new file with mode: 0644]
Lib/importlib/test/extension/test_case_sensitivity.py [new file with mode: 0644]
Lib/importlib/test/extension/test_finder.py [new file with mode: 0644]
Lib/importlib/test/extension/test_loader.py [new file with mode: 0644]
Lib/importlib/test/extension/test_path_hook.py [new file with mode: 0644]
Lib/importlib/test/finder_tests.py [new file with mode: 0644]
Lib/importlib/test/frozen/__init__.py [new file with mode: 0644]
Lib/importlib/test/frozen/support.py [new file with mode: 0644]
Lib/importlib/test/frozen/test_finder.py [new file with mode: 0644]
Lib/importlib/test/frozen/test_loader.py [new file with mode: 0644]
Lib/importlib/test/import_/__init__.py [new file with mode: 0644]
Lib/importlib/test/import_/test___package__.py [new file with mode: 0644]
Lib/importlib/test/import_/test_caching.py [new file with mode: 0644]
Lib/importlib/test/import_/test_fromlist.py [new file with mode: 0644]
Lib/importlib/test/import_/test_meta_path.py [new file with mode: 0644]
Lib/importlib/test/import_/test_packages.py [new file with mode: 0644]
Lib/importlib/test/import_/test_path.py [new file with mode: 0644]
Lib/importlib/test/import_/test_relative_imports.py [new file with mode: 0644]
Lib/importlib/test/source/__init__.py [new file with mode: 0644]
Lib/importlib/test/source/test_case_sensitivity.py [new file with mode: 0644]
Lib/importlib/test/source/test_finder.py [new file with mode: 0644]
Lib/importlib/test/source/test_loader.py [new file with mode: 0644]
Lib/importlib/test/source/test_path_hook.py [new file with mode: 0644]
Lib/importlib/test/source/test_reload.py [new file with mode: 0644]
Lib/importlib/test/source/test_source_encoding.py [new file with mode: 0644]
Lib/importlib/test/support.py [new file with mode: 0644]
Lib/importlib/test/test_api.py [new file with mode: 0644]
Lib/test/test_importlib.py [new file with mode: 0644]

diff --git a/Lib/importlib/NOTES b/Lib/importlib/NOTES
new file mode 100644 (file)
index 0000000..134f112
--- /dev/null
@@ -0,0 +1,78 @@
+to do
+/////
+
+* Write importlib.__import__
+
+* Document
+    + Package.
+    + import_module
+    + __import__
+
+* Create reasonable base tests that all finders and loaders must pass so
+  that various implementations can just subclass as needed.
+
+* Expose built-in and frozen importers.
+    + Make staticmethods so that class can be used directly.
+
+* Reorganize support code.
+    + Separate general support code and importer-specific (e.g. source) support
+      code.
+        - Create support modules for each subdirectory (as needed).
+    + Add a file loader mock that returns monotonically increasing mtime.
+        - Use in source/test_reload.
+        - Use in source/test_load_module_mixed.
+
+* API simplification?
+    + read_source -> get_data/source_path
+    + read_bytecode -> get_data/bytecode_path
+    + write_bytecode -> complete set of bytes for bytecode instead of
+      individual arguments.
+
+* Implement PEP 302 protocol for loaders (should just be a matter of testing).
+    + Built-in.
+    + Frozen.
+    + Extension.
+    + Source/bytecode.
+
+* Create meta_path importer for sys.path.
+
+* OPTIMIZE!
+  + Write benchmark suite.
+  + Fast path common cases.
+    - Absolute name from sys.path.
+    - Relative name from sys.path.
+
+* Public API (w/ docs!)
+  + abc
+      - Finder
+        * find_module
+      - Loader
+        * load_module
+      - ResourceLoader(Loader)
+        * get_data
+      - InspectLoader(Loader)
+        * is_package
+        * get_code
+        * get_source
+      - (?) SourceLoader(ResourceLoader)
+        * source_path
+        * bytecode_path
+        * write_bytecode
+  + util
+      - get_module decorator (new name)
+      - check_name decorator (new name)
+  + hooks (?)
+      - (?) Chained path hook/finder
+      - BuiltinImporter
+      - FrozenImporter
+      - (?) FileFinder
+      - Extensions importers
+          * ExtensionFinder
+          * (?) Loader
+      - Source/bytecode importers
+          * SourceFinder
+          * (?) Loader
+  + __init__
+      - __import__
+      - import_module (backport to 2.7)
+      - resolve_name (backport to 2.7)
diff --git a/Lib/importlib/__init__.py b/Lib/importlib/__init__.py
new file mode 100644 (file)
index 0000000..b59c9c4
--- /dev/null
@@ -0,0 +1,133 @@
+"""A pure Python implementation of import.
+
+References on import:
+
+    * Language reference
+          http://docs.python.org/ref/import.html
+    * __import__ function
+          http://docs.python.org/lib/built-in-funcs.html
+    * Packages
+          http://www.python.org/doc/essays/packages.html
+    * PEP 235: Import on Case-Insensitive Platforms
+          http://www.python.org/dev/peps/pep-0235
+    * PEP 275: Import Modules from Zip Archives
+          http://www.python.org/dev/peps/pep-0273
+    * PEP 302: New Import Hooks
+          http://www.python.org/dev/peps/pep-0302/
+    * PEP 328: Imports: Multi-line and Absolute/Relative
+          http://www.python.org/dev/peps/pep-0328
+
+"""
+from . import _bootstrap
+
+# XXX Temporary functions that should eventually be removed.
+import os
+import re
+import tokenize
+
+def _set__import__():
+    """Set __import__ to an instance of Import."""
+    global original__import__
+    original__import__ = __import__
+    __builtins__['__import__'] = Import()
+
+
+def _reset__import__():
+    """Set __import__ back to the original implementation (assumes
+    _set__import__ was called previously)."""
+    __builtins__['__import__'] = original__import__
+
+
+def _case_ok(directory, check):
+    """Check if the directory contains something matching 'check'.
+
+    No check is done if the file/directory exists or not.
+
+    """
+    if 'PYTHONCASEOK' in os.environ:
+        return True
+    elif check in os.listdir(directory):
+        return True
+    return False
+
+
+def _w_long(x):
+    """Convert a 32-bit integer to little-endian.
+
+    XXX Temporary until marshal's long functions are exposed.
+
+    """
+    x = int(x)
+    int_bytes = []
+    int_bytes.append(x & 0xFF)
+    int_bytes.append((x >> 8) & 0xFF)
+    int_bytes.append((x >> 16) & 0xFF)
+    int_bytes.append((x >> 24) & 0xFF)
+    return bytearray(int_bytes)
+
+
+def _r_long(int_bytes):
+    """Convert 4 bytes in little-endian to an integer.
+
+    XXX Temporary until marshal's long function are exposed.
+
+    """
+    x = int_bytes[0]
+    x |= int_bytes[1] << 8
+    x |= int_bytes[2] << 16
+    x |= int_bytes[3] << 24
+    return x
+
+
+def import_module(name, package=None):
+    """Import a module.
+
+    The 'package' argument is used to resolve relative import names. Typically
+    this is the __package__ attribute of the module making the function call.
+
+    """
+    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 = Import._resolve_name(name[level:], package, level)
+    __import__(name)
+    return sys.modules[name]
+
+
+
+# Required built-in modules.
+try:
+    import posix as _os
+except ImportError:
+    try:
+        import nt as _os
+    except ImportError:
+        try:
+            import os2 as _os
+        except ImportError:
+            raise ImportError('posix, nt, or os2 module required for importlib')
+_bootstrap._os = _os
+import imp, sys, marshal, errno, _fileio
+_bootstrap.imp = imp
+_bootstrap.sys = sys
+_bootstrap.marshal = marshal
+_bootstrap.errno = errno
+_bootstrap._fileio = _fileio
+import _warnings
+_bootstrap._warnings = _warnings
+
+
+from os import sep
+# For os.path.join replacement; pull from Include/osdefs.h:SEP .
+_bootstrap.path_sep = sep
+
+_bootstrap._case_ok = _case_ok
+marshal._w_long = _w_long
+marshal._r_long = _r_long
+
+from ._bootstrap import *
diff --git a/Lib/importlib/_bootstrap.py b/Lib/importlib/_bootstrap.py
new file mode 100644 (file)
index 0000000..68f0017
--- /dev/null
@@ -0,0 +1,997 @@
+"""Core implementation of import.
+
+This module is NOT meant to be directly imported! It has been designed such
+that it can be bootstrapped into Python as the implementation of import. As
+such it requires the injection of specific modules and attributes in order to
+work. One should use importlib as the public-facing version of this module.
+
+"""
+
+# Injected modules are '_warnings', 'imp', 'sys', 'marshal', 'errno', and '_os'
+# (a.k.a. 'posix', 'nt' or 'os2').
+# Injected attribute is path_sep.
+#
+# When editing this code be aware that code executed at import time CANNOT
+# reference any injected objects! This includes not only global code but also
+# anything specified at the class level.
+
+
+# XXX Could also expose Modules/getpath.c:joinpath()
+def _path_join(*args):
+    """Replacement for os.path.join."""
+    return path_sep.join(x[:-len(path_sep)] if x.endswith(path_sep) else x
+                            for x in args)
+
+
+def _path_exists(path):
+    """Replacement for os.path.exists."""
+    try:
+        _os.stat(path)
+    except OSError:
+        return False
+    else:
+        return True
+
+
+def _path_is_mode_type(path, mode):
+    """Test whether the path is the specified mode type."""
+    try:
+        stat_info = _os.stat(path)
+    except OSError:
+        return False
+    return (stat_info.st_mode & 0o170000) == mode
+
+
+# XXX Could also expose Modules/getpath.c:isfile()
+def _path_isfile(path):
+    """Replacement for os.path.isfile."""
+    return _path_is_mode_type(path, 0o100000)
+
+
+# XXX Could also expose Modules/getpath.c:isdir()
+def _path_isdir(path):
+    """Replacement for os.path.isdir."""
+    return _path_is_mode_type(path, 0o040000)
+
+
+def _path_without_ext(path, ext_type):
+    """Replacement for os.path.splitext()[0]."""
+    for suffix in suffix_list(ext_type):
+        if path.endswith(suffix):
+            return path[:-len(suffix)]
+    else:
+        raise ValueError("path is not of the specified type")
+
+
+def _path_absolute(path):
+    """Replacement for os.path.abspath."""
+    if not path:
+        path = _os.getcwd()
+    try:
+        return _os._getfullpathname(path)
+    except AttributeError:
+        if path.startswith('/'):
+            return path
+        else:
+            return _path_join(_os.getcwd(), path)
+
+
+class closing:
+
+    """Simple replacement for contextlib.closing."""
+
+    def __init__(self, obj):
+        self.obj = obj
+
+    def __enter__(self):
+        return self.obj
+
+    def __exit__(self, *args):
+        self.obj.close()
+
+
+class _BuiltinFrozenBaseLoader(object):
+
+    """Base class for meta_path loaders for built-in and frozen modules.
+
+    Subclasses must implement:
+
+        * _find(fullname:str) -> bool
+            Finder which returns whether the class can handle the module.
+
+        * _load(fullname:str) -> module
+            Loader which returns the loaded module. The check for sys.modules
+            does not need to be handled by this method.
+
+        * type_:str
+            Name of the type of module being handled. Used in error messages.
+
+    """
+
+    def find_module(self, fullname, path=None):
+        """Find a module."""
+        if not self._find(fullname):
+            return None
+        return self
+
+    def load_module(self, fullname):
+        """Load a module."""
+        try:
+            return sys.modules[fullname]
+        except KeyError:
+            pass
+        mod = self._load(fullname)
+        if not mod:
+            raise ImportError("expected {0} module not "
+                                "loaded".format(self.type_))
+        return mod
+
+
+class BuiltinImporter(_BuiltinFrozenBaseLoader):
+
+    """Meta path loader for built-in modules."""
+
+    type_ = "built-in"
+
+    def __init__(self):
+        """Set the methods needed by the class.
+
+        Cannot be set at the class level because the imp module is not
+        necessarily injected until after the class is created.
+
+        """
+        self._find = imp.is_builtin
+        self._load = imp.init_builtin
+
+    def find_module(self, fullname, path=None):
+        """Try to find the built-in module.
+
+        If 'path' is ever specified then the search is considered a failure.
+
+        """
+        if path is not None:
+            return None
+        return super().find_module(fullname, path)
+
+    def load_module(self, fullname):
+        """Load a built-in module."""
+        if fullname not in sys.builtin_module_names:
+            raise ImportError("{0} is not a built-in module".format(fullname))
+        return super().load_module(fullname)
+
+
+class FrozenImporter(_BuiltinFrozenBaseLoader):
+
+    """Meta path class for importing frozen modules."""
+
+    type_ = 'frozen'
+
+    def __init__(self):
+        """Specify the methods needed by the superclass.
+
+        Because imp may not be injected until after class creation these
+        methods cannot be set at the class level.
+
+        """
+        self._find = imp.is_frozen
+        self._load = imp.init_frozen
+
+    def load_module(self, fullname):
+        """Load a frozen module."""
+        if not self.find_module(fullname):
+            raise ImportError("{0} is not a frozen module".format(fullname))
+        return super().load_module(fullname)
+
+
+class ChainedImporter(object):
+
+    """Finder that sequentially calls other finders."""
+
+    def __init__(self, *importers):
+        self._importers = importers
+
+    def find_module(self, fullname, path=None):
+        for importer in self._importers:
+            result = importer.find_module(fullname, path)
+            if result:
+                return result
+        else:
+            return None
+
+
+# XXX Don't make filesystem-specific and instead make generic for any path
+#     hooks.
+def chaining_fs_path_hook(*path_hooks):
+    """Create a closure which calls the path hooks sequentially looking for
+    which path hooks can handle a path entry.
+
+
+    Passed-in path hooks work as any other path hooks, raising ImportError if
+    they cannot handle the path, otherwise returning a finder.
+
+    """
+    def chained_fs_path_hook(path_entry):
+        """Closure which sees which of the captured path hooks can handle the
+        path entry."""
+        absolute_path = _path_absolute(path_entry)
+        if not _path_isdir(absolute_path):
+            raise ImportError("only directories are supported")
+        accepted = []
+        for path_hook in path_hooks:
+            try:
+                accepted.append(path_hook(absolute_path))
+            except ImportError:
+                continue
+        if not accepted:
+            raise ImportError("no path hooks could handle %s" % path_entry)
+        return ChainedImporter(*accepted)
+    return chained_fs_path_hook
+
+
+def check_name(method):
+    """Decorator to verify that the module being requested matches the one the
+    loader can handle.
+
+    The first argument (self) must define _name which the second argument is
+    comapred against. If the comparison fails then ImportError is raised.
+
+    """
+    def inner(self, name, *args, **kwargs):
+        if self._name != name:
+            raise ImportError("loader cannot handle %s" % name)
+        return method(self, name, *args, **kwargs)
+    inner.__name__ = method.__name__
+    inner.__doc__ = method.__doc__
+    inner.__dict__.update(method.__dict__)
+    return inner
+
+
+class _ExtensionFileLoader(object):
+
+    """Loader for extension modules.
+
+    The constructor is designed to work with FileImporter.
+
+    """
+
+    def __init__(self, name, path, is_pkg):
+        """Initialize the loader.
+
+        If is_pkg is True then an exception is raised as extension modules
+        cannot be the __init__ module for an extension module.
+
+        """
+        self._name = name
+        self._path = path
+        if is_pkg:
+            raise ValueError("extension modules cannot be packages")
+
+    @check_name
+    def load_module(self, fullname):
+        """Load an extension module."""
+        assert self._name == fullname
+        try:
+            module = imp.load_dynamic(fullname, self._path)
+            module.__loader__ = self
+            return module
+        except:
+            # If an error occurred, don't leave a partially initialized module.
+            if fullname in sys.modules:
+                del sys.modules[fullname]
+            raise
+
+    @check_name
+    def is_package(self, fullname):
+        """Return False as an extension module can never be a package."""
+        return False
+
+    @check_name
+    def get_code(self, fullname):
+        """Return None as an extension module cannot create a code object."""
+        return None
+
+    @check_name
+    def get_source(self, fullname):
+        """Return None as extension modules have no source code."""
+        return None
+
+
+def suffix_list(suffix_type):
+    """Return a list of file suffixes based on the imp file type."""
+    return [suffix[0] for suffix in imp.get_suffixes()
+            if suffix[2] == suffix_type]
+
+
+# XXX Need a better name.
+def get_module(fxn):
+    """Decorator to handle selecting the proper module for load_module
+    implementations.
+
+    Decorated modules are passed the module to use instead of the module name.
+    The module is either from sys.modules if it already exists (for reloading)
+    or is a new module which has __name__ set. If any exception is raised by
+    the decorated method then __loader__, __name__, __file__, and __path__ are
+    all restored on the module to their original values.
+
+    """
+    def decorated(self, fullname):
+        module = sys.modules.get(fullname)
+        is_reload = bool(module)
+        if not is_reload:
+            # This must be done before open() is called as the 'io' module
+            # implicitly imports 'locale' and would otherwise trigger an
+            # infinite loop.
+            module = imp.new_module(fullname)
+            module.__name__ = fullname
+            sys.modules[fullname] = module
+        else:
+            original_values = {}
+            modified_attrs = ['__loader__', '__name__', '__file__', '__path__']
+            for attr in modified_attrs:
+                try:
+                    original_values[attr] = getattr(module, attr)
+                except AttributeError:
+                    pass
+        try:
+            return fxn(self, module)
+        except:
+            if not is_reload:
+                del sys.modules[fullname]
+            else:
+                for attr in modified_attrs:
+                    if attr in original_values:
+                        setattr(module, attr, original_values[attr])
+                    elif hasattr(module, attr):
+                        delattr(module, attr)
+            raise
+    return decorated
+
+
+class _PyFileLoader(object):
+    # XXX Still smart to have this as a separate class?  Or would it work
+    # better to integrate with PyFileImporter?  Could cache _is_pkg info.
+    # FileImporter can be changed to return self instead of a specific loader
+    # call.  Otherwise _base_path can be calculated on the fly without issue if
+    # it is known whether a module should be treated as a path or package to
+    # minimize stat calls.  Could even go as far as to stat the directory the
+    # importer is in to detect changes and then cache all the info about what
+    # files were found (if stating directories is platform-dependent).
+
+    """Load a Python source or bytecode file."""
+
+    def __init__(self, name, path, is_pkg):
+        self._name = name
+        self._is_pkg = is_pkg
+        # Figure out the base path based on whether it was source or bytecode
+        # that was found.
+        try:
+            self._base_path = _path_without_ext(path, imp.PY_SOURCE)
+        except ValueError:
+            self._base_path = _path_without_ext(path, imp.PY_COMPILED)
+
+    def _find_path(self, ext_type):
+        """Find a path from the base path and the specified extension type that
+        exists, returning None if one is not found."""
+        for suffix in suffix_list(ext_type):
+            path = self._base_path + suffix
+            if _path_exists(path):
+                return path
+        else:
+            return None
+
+    def _source_path(self):
+        """Return the path to an existing source file for the module, or None
+        if one cannot be found."""
+        # Not a property so that it is easy to override.
+        return self._find_path(imp.PY_SOURCE)
+
+    def _bytecode_path(self):
+        """Return the path to a bytecode file, or None if one does not
+        exist."""
+        # Not a property for easy overriding.
+        return self._find_path(imp.PY_COMPILED)
+
+    @check_name
+    @get_module
+    def load_module(self, module):
+        """Load a Python source or bytecode module."""
+        source_path = self._source_path()
+        bytecode_path = self._bytecode_path()
+        code_object = self.get_code(module.__name__)
+        module.__file__ = source_path if source_path else bytecode_path
+        module.__loader__ = self
+        if self._is_pkg:
+            module.__path__  = [module.__file__.rsplit(path_sep, 1)[0]]
+            module.__package__ = module.__name__
+        elif '.' in module.__name__:
+            module.__package__ = module.__name__.rsplit('.', 1)[0]
+        else:
+            module.__package__ = None
+        exec(code_object, module.__dict__)
+        return module
+
+    @check_name
+    def source_mtime(self, name):
+        """Return the modification time of the source for the specified
+        module."""
+        source_path = self._source_path()
+        if not source_path:
+            return None
+        return int(_os.stat(source_path).st_mtime)
+
+    @check_name
+    def get_source(self, fullname):
+        """Return the source for the module as a string.
+
+        Return None if the source is not available. Raise ImportError if the
+        laoder cannot handle the specified module.
+
+        """
+        source_path = self._source_path()
+        if source_path is None:
+            return None
+        import tokenize
+        with closing(_fileio_FileIO(source_path, 'r')) as file:
+            encoding, lines = tokenize.detect_encoding(file.readline)
+        # XXX Will fail when passed to compile() if the encoding is
+        # anything other than UTF-8.
+        return open(source_path, encoding=encoding).read()
+
+    @check_name
+    def read_source(self, fullname):
+        """Return the source for the specified module as bytes along with the
+        path where the source came from.
+
+        The returned path is used by 'compile' for error messages.
+
+        """
+        source_path = self._source_path()
+        if source_path is None:
+            return None
+        with closing(_fileio._FileIO(source_path, 'r')) as bytes_file:
+            return bytes_file.read(), source_path
+
+    @check_name
+    def read_bytecode(self, name):
+        """Return the magic number, timestamp, and the module bytecode for the
+        module.
+
+        Raises ImportError (just like get_source) if the laoder cannot handle
+        the module. Returns None if there is no bytecode.
+
+        """
+        path = self._bytecode_path()
+        if path is None:
+            return None
+        file = _fileio._FileIO(path, 'r')
+        try:
+            with closing(file) as bytecode_file:
+                data = bytecode_file.read()
+            return data[:4], marshal._r_long(data[4:8]), data[8:]
+        except AttributeError:
+            return None
+
+    @check_name
+    def write_bytecode(self, name, magic, timestamp, data):
+        """Write out 'data' for the specified module using the specific
+        timestamp, returning a boolean
+        signifying if the write-out actually occurred.
+
+        Raises ImportError (just like get_source) if the specified module
+        cannot be handled by the loader.
+
+        """
+        bytecode_path = self._bytecode_path()
+        if not bytecode_path:
+            bytecode_path = self._base_path + suffix_list(imp.PY_COMPILED)[0]
+        file = _fileio._FileIO(bytecode_path, 'w')
+        try:
+            with closing(file) as bytecode_file:
+                bytecode_file.write(magic)
+                bytecode_file.write(marshal._w_long(timestamp))
+                bytecode_file.write(data)
+                return True
+        except IOError as exc:
+            if exc.errno == errno.EACCES:
+                return False
+            else:
+                raise
+
+    # XXX Take an optional argument to flag whether to write bytecode?
+    @check_name
+    def get_code(self, name):
+        """Return the code object for the module.
+
+            'self' must implement:
+
+            * read_bytecode(name:str) -> (int, int, bytes) or None
+                Return the magic number, timestamp, and bytecode for the
+                module. None is returned if not bytecode is available.
+
+            * source_mtime(name:str) -> int
+                Return the last modification time for the source of the module.
+                Returns None if their is no source.
+
+            * read_source(name:str) -> (bytes, str)
+                Return the source code for the module and the path to use in
+                the call to 'compile'. Not called if source_mtime returned
+                None.
+
+            * write_bytecode(name:str, magic:bytes, timestamp:int, data:str)
+                Write out bytecode for the module with the specified magic
+                number and timestamp. Not called if sys.dont_write_bytecode is
+                True.
+
+        """
+        # XXX Care enough to make sure this call does not happen if the magic
+        #     number is bad?
+        source_timestamp = self.source_mtime(name)
+        # Try to use bytecode if it is available.
+        bytecode_tuple = self.read_bytecode(name)
+        if bytecode_tuple:
+            magic, pyc_timestamp, bytecode = bytecode_tuple
+            try:
+                # Verify that the magic number is valid.
+                if imp.get_magic() != magic:
+                    raise ImportError("bad magic number")
+                # Verify that the bytecode is not stale (only matters when
+                # there is source to fall back on.
+                if source_timestamp:
+                    if pyc_timestamp < source_timestamp:
+                        raise ImportError("bytcode is stale")
+            except ImportError:
+                # If source is available give it a shot.
+                if source_timestamp is not None:
+                    pass
+                else:
+                    raise
+            else:
+                # Bytecode seems fine, so try to use it.
+                # XXX If the bytecode is ill-formed, would it be beneficial to
+                #     try for using source if available and issue a warning?
+                return marshal.loads(bytecode)
+        elif source_timestamp is None:
+            raise ImportError("no source or bytecode available to create code "
+                                "object for {0!r}".format(name))
+        # Use the source.
+        source, source_path = self.read_source(name)
+        # Convert to universal newlines.
+        line_endings = b'\n'
+        for index, c in enumerate(source):
+            if c == ord(b'\n'):
+                break
+            elif c == ord(b'\r'):
+                line_endings = b'\r'
+                try:
+                    if source[index+1] == ord(b'\n'):
+                        line_endings += b'\n'
+                except IndexError:
+                    pass
+                break
+        if line_endings != b'\n':
+            source = source.replace(line_endings, b'\n')
+        code_object = compile(source, source_path, 'exec', dont_inherit=True)
+        # Generate bytecode and write it out.
+        if not sys.dont_write_bytecode:
+            data = marshal.dumps(code_object)
+            self.write_bytecode(name, imp.get_magic(), source_timestamp, data)
+        return code_object
+
+    def get_data(self, path):
+        """Return the data from path as raw bytes."""
+        return _fileio._FileIO(path, 'r').read()
+
+    @check_name
+    def is_package(self, fullname):
+        """Return a boolean based on whether the module is a package.
+
+        Raises ImportError (like get_source) if the loader cannot handle the
+        package.
+
+        """
+        return self._is_pkg
+
+
+class FileImporter(object):
+
+    """Base class for file importers.
+
+    Subclasses are expected to define the following attributes:
+
+        * _suffixes
+            Sequence of file suffixes whose order will be followed.
+
+        * _possible_package
+            True if importer should check for packages.
+
+        * _loader
+            A callable that takes the module name, a file path, and whether
+            the path points to a package and returns a loader for the module
+            found at that path.
+
+    """
+
+    def __init__(self, path_entry):
+        """Initialize an importer for the passed-in sys.path entry (which is
+        assumed to have already been verified as an existing directory).
+
+        Can be used as an entry on sys.path_hook.
+
+        """
+        self._path_entry = path_entry
+
+    def find_module(self, fullname, path=None):
+        tail_module = fullname.rsplit('.', 1)[-1]
+        package_directory = None
+        if self._possible_package:
+            for ext in self._suffixes:
+                package_directory = _path_join(self._path_entry, tail_module)
+                init_filename = '__init__' + ext
+                package_init = _path_join(package_directory, init_filename)
+                if (_path_isfile(package_init) and
+                        _case_ok(self._path_entry, tail_module) and
+                        _case_ok(package_directory, init_filename)):
+                    return self._loader(fullname, package_init, True)
+        for ext in self._suffixes:
+            file_name = tail_module + ext
+            file_path = _path_join(self._path_entry, file_name)
+            if (_path_isfile(file_path) and
+                    _case_ok(self._path_entry, file_name)):
+                return self._loader(fullname, file_path, False)
+        else:
+            # Raise a warning if it matches a directory w/o an __init__ file.
+            if (package_directory is not None and
+                    _path_isdir(package_directory) and
+                    _case_ok(self._path_entry, tail_module)):
+                _warnings.warn("Not importing directory %s: missing __init__"
+                                % package_directory, ImportWarning)
+            return None
+
+
+class ExtensionFileImporter(FileImporter):
+
+    """Importer for extension files."""
+
+    _possible_package = False
+    _loader = _ExtensionFileLoader
+
+    def __init__(self, path_entry):
+        # Assigning to _suffixes here instead of at the class level because
+        # imp is not imported at the time of class creation.
+        self._suffixes = suffix_list(imp.C_EXTENSION)
+        super(ExtensionFileImporter, self).__init__(path_entry)
+
+
+class PyFileImporter(FileImporter):
+
+    """Importer for source/bytecode files."""
+
+    _possible_package = True
+    _loader = _PyFileLoader
+
+    def __init__(self, path_entry):
+        # Lack of imp during class creation means _suffixes is set here.
+        # Make sure that Python source files are listed first!  Needed for an
+        # optimization by the loader.
+        self._suffixes = suffix_list(imp.PY_SOURCE)
+        self._suffixes += suffix_list(imp.PY_COMPILED)
+        super(PyFileImporter, self).__init__(path_entry)
+
+
+class ImportLockContext(object):
+
+    """Context manager for the import lock."""
+
+    def __enter__(self):
+        """Acquire the import lock."""
+        imp.acquire_lock()
+
+    def __exit__(self, exc_type, exc_value, exc_traceback):
+        """Release the import lock regardless of any raised exceptions."""
+        imp.release_lock()
+
+
+class Import(object):
+
+    """Class that implements the __import__ interface.
+
+    Backwards compatibility is maintained  by extending sys.meta_path
+    interally (for handling built-in and frozen modules) and providing a
+    default path hooks entry for extension modules, .py, and .pyc
+    files.  Both are controlled during instance initialization.
+
+    """
+
+    def __init__(self, default_path_hook=None,
+                 extended_meta_path=None):
+        """Store a default path hook entry and a sequence to internally extend
+        sys.meta_path by (passing in None uses default importers)."""
+        if extended_meta_path is None:
+            self.extended_meta_path = BuiltinImporter(), FrozenImporter()
+        else:
+            self.extended_meta_path = extended_meta_path
+        self.default_path_hook = default_path_hook
+        if self.default_path_hook is None:
+            # Create a handler to deal with extension modules, .py, and .pyc
+            # files.  Built-in and frozen modules are handled by sys.meta_path
+            # entries.
+            importers = [ExtensionFileImporter, PyFileImporter]
+            self.default_path_hook = chaining_fs_path_hook(*importers)
+
+    def _search_meta_path(self, name, path=None):
+        """Check the importers on sys.meta_path for a loader along with the
+        extended meta path sequence stored within this instance.
+
+        The extended sys.meta_path entries are searched after the entries on
+        sys.meta_path.
+
+        """
+        for entry in (tuple(sys.meta_path) + self.extended_meta_path):
+            loader = entry.find_module(name, path)
+            if loader:
+                return loader
+        else:
+            raise ImportError("No module named %s" % name)
+
+    def _sys_path_importer(self, path_entry):
+        """Return the importer for the specified path, from
+        sys.path_importer_cache if possible.
+
+        If None is stored in sys.path_importer_cache then use the default path
+        hook.
+
+        """
+        try:
+            # See if an importer is cached.
+            importer = sys.path_importer_cache[path_entry]
+            # If None was returned, use default importer factory.
+            if importer is None:
+                return self.default_path_hook(path_entry)
+            else:
+                return importer
+        except KeyError:
+            # No cached importer found; try to get a new one from
+            # sys.path_hooks or imp.NullImporter.
+            for importer_factory in (sys.path_hooks + [imp.NullImporter]):
+                try:
+                    importer = importer_factory(path_entry)
+                    sys.path_importer_cache[path_entry] = importer
+                    return importer
+                except ImportError:
+                    continue
+            else:
+                # No importer factory on sys.path_hooks works; use the default
+                # importer factory and store None in sys.path_importer_cache.
+                try:
+                    importer = self.default_path_hook(path_entry)
+                    sys.path_importer_cache[path_entry] = None
+                    return importer
+                except ImportError:
+                    raise ImportError("no importer found for %s" % path_entry)
+
+    def _search_std_path(self, name, path=None):
+        """Check sys.path or 'path' (depending if 'path' is set) for the
+        named module and return its loader."""
+        if path:
+            search_paths = path
+        else:
+            search_paths = sys.path
+        for entry in search_paths:
+            try:
+                importer = self._sys_path_importer(entry)
+            except ImportError:
+                continue
+            loader = importer.find_module(name)
+            if loader:
+                return loader
+        else:
+            raise ImportError("No module named %s" % name)
+
+    def module_from_cache(self, name):
+        """Try to return the named module from sys.modules.
+
+        Return False if the module is not in the cache.
+        """
+        if name in sys.modules:
+            return sys.modules[name]
+        else:
+            return False
+
+    def post_import(self, module):
+        """Perform any desired post-import processing on the module."""
+        return module
+
+    def _import_module(self, name, path=None):
+        """Import the specified module with no handling of parent modules.
+
+        If None is set for a value in sys.modules (to signify that a relative
+        import was attempted and failed) then ImportError is raised.
+
+        """
+        cached_module = self.module_from_cache(name)
+        if cached_module is not False:
+            if cached_module is None:
+                raise ImportError("relative import redirect")
+            else:
+                return cached_module
+        try:
+            # Attempt to find a loader on sys.meta_path.
+            loader = self._search_meta_path(name, path)
+        except ImportError:
+            # sys.meta_path search failed.  Attempt to find a loader on
+            # sys.path.  If this fails then module cannot be found.
+            loader = self._search_std_path(name, path)
+        # A loader was found.  It is the loader's responsibility to have put an
+        # entry in sys.modules.
+        module = self.post_import(loader.load_module(name))
+        # 'module' could be something like None.
+        if not hasattr(module, '__name__'):
+            return module
+        # Set __package__.
+        if not hasattr(module, '__package__') or module.__package__ is None:
+            if hasattr(module, '__path__'):
+                module.__package__ = module.__name__
+            elif '.' in module.__name__:
+                pkg_name = module.__name__.rsplit('.', 1)[0]
+                module.__package__ = pkg_name
+            else:
+                module.__package__ = None
+        return module
+
+
+    def _import_full_module(self, name):
+        """Import a module and set it on its parent if needed."""
+        path_list = None
+        parent_name = name.rsplit('.', 1)[0]
+        parent = None
+        if parent_name != name:
+            parent = sys.modules[parent_name]
+            try:
+                path_list = parent.__path__
+            except AttributeError:
+                pass
+        self._import_module(name, path_list)
+        module = sys.modules[name]
+        if parent:
+            tail = name.rsplit('.', 1)[-1]
+            setattr(parent, tail, module)
+
+    def _find_package(self, name, has_path):
+        """Return the package that the caller is in or None."""
+        if has_path:
+            return name
+        elif '.' in name:
+            return name.rsplit('.', 1)[0]
+        else:
+            return None
+
+    @staticmethod
+    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 _return_module(self, absolute_name, relative_name, fromlist):
+        """Return the proper module based on what module was requested (and its
+        absolute module name), who is requesting it, and whether any specific
+        attributes were specified.
+
+        The semantics of this method revolve around 'fromlist'.  When it is
+        empty, the module up to the first dot is to be returned.  When the
+        module being requested is an absolute name this is simple (and
+        relative_name is an empty string).  But if the requested module was
+        a relative import (as signaled by relative_name having a non-false
+        value), then the name up to the first dot in the relative name resolved
+        to an absolute name is to be returned.
+
+        When fromlist is not empty and the module being imported is a package,
+        then the values
+        in fromlist need to be checked for.  If a value is not a pre-existing
+        attribute a relative import is attempted.  If it fails then suppressed
+        the failure silently.
+
+        """
+        if not fromlist:
+            if relative_name:
+                absolute_base = absolute_name.rpartition(relative_name)[0]
+                relative_head = relative_name.split('.', 1)[0]
+                to_return = absolute_base + relative_head
+            else:
+                to_return = absolute_name.split('.', 1)[0]
+            return sys.modules[to_return]
+        # When fromlist is not empty, return the actual module specified in
+        # the import.
+        else:
+            module = sys.modules[absolute_name]
+            if hasattr(module, '__path__') and hasattr(module, '__name__'):
+                # When fromlist has a value and the imported module is a
+                # package, then if a name in fromlist is not found as an
+                # attribute on module, try a relative import to find it.
+                # Failure is fine and the exception is suppressed.
+                check_for = list(fromlist)
+                if '*' in check_for and hasattr(module, '__all__'):
+                    check_for.extend(module.__all__)
+                for item in check_for:
+                    if item == '*':
+                        continue
+                    if not hasattr(module, item):
+                        resolved_name = self._resolve_name(item,
+                                                            module.__name__, 1)
+                        try:
+                            self._import_full_module(resolved_name)
+                        except ImportError:
+                            pass
+            return module
+
+    def __call__(self, name, globals={}, locals={}, fromlist=[], level=0):
+        """Import a module.
+
+        The 'name' argument is the name of the module to be imported (e.g.,
+        'foo' in ``import foo`` or ``from foo import ...``).
+
+        'globals' and 'locals' are the global and local namespace dictionaries
+        of the module where the import statement appears.  'globals' is used to
+        introspect the __path__ and __name__ attributes of the module making
+        the call.  'local's is ignored.
+
+        'fromlist' lists any specific objects that are to eventually be put
+        into the namespace (e.g., ``from for.bar import baz`` would have 'baz'
+        in the fromlist, and this includes '*').  An entry of '*' will lead to
+        a check for __all__ being defined on the module.  If it is defined then
+        the values in __all__ will be checked to make sure that all values are
+        attributes on the module, attempting a module import relative to 'name'
+        to set that attribute.
+
+        When 'name' is a dotted name, there are two different situations to
+        consider for the return value.  One is when the fromlist is empty.
+        In this situation the import statement imports and returns the name up
+        to the first dot.  All subsequent names are imported but set as
+        attributes as needed on parent modules.  When fromlist is not empty
+        then the module represented by the full dotted name is returned.
+
+        'level' represents possible relative imports.
+        A value of 0 is for absolute module names. Any positive value
+        represents the number of dots listed in the relative import statement
+        (e.g. has a value of 2 for ``from .. import foo``).
+
+        """
+        if not name and level < 1:
+            raise ValueError("Empty module name")
+        is_pkg = True if '__path__' in globals else False
+        caller_name = globals.get('__name__')
+        package = globals.get('__package__')
+        if caller_name and not package:
+            package = self._find_package(caller_name, '__path__' in globals)
+        if package and package not in sys.modules:
+            if not hasattr(package, 'rsplit'):
+                raise ValueError("__package__ not set to a string")
+            msg = ("Parent module {0!r} not loaded, "
+                    "cannot perform relative import")
+            raise SystemError(msg.format(package))
+        with ImportLockContext():
+            if level:
+                imported_name = self._resolve_name(name, package, level)
+            else:
+                imported_name = name
+            parent_name = imported_name.rsplit('.', 1)[0]
+            if parent_name != imported_name and parent_name not in sys.modules:
+                self.__call__(parent_name, level=0)
+            # This call will also handle setting the attribute on the
+            # package.
+            self._import_full_module(imported_name)
+            relative_name = '' if imported_name == name else name
+            return self._return_module(imported_name, relative_name, fromlist)
+
+# XXX Eventually replace with a proper __all__ value (i.e., don't expose os
+# replacements but do expose _ExtensionFileLoader, etc. for testing).
+__all__ = [obj for obj in globals().keys() if not obj.startswith('__')]
diff --git a/Lib/importlib/test/__init__.py b/Lib/importlib/test/__init__.py
new file mode 100644 (file)
index 0000000..6492707
--- /dev/null
@@ -0,0 +1,26 @@
+import os.path
+import sys
+import unittest
+
+
+def test_suite(package=__package__, directory=os.path.dirname(__file__)):
+    suite = unittest.TestSuite()
+    for name in os.listdir(directory):
+        path = os.path.join(directory, name)
+        if os.path.isfile(path) and name.startswith('test_'):
+            submodule_name = os.path.splitext(name)[0]
+            module_name = "{0}.{1}".format(package, submodule_name)
+            __import__(module_name, level=0)
+            module_tests = unittest.findTestCases(sys.modules[module_name])
+            suite.addTest(module_tests)
+        elif os.path.isdir(path):
+            package_name = "{0}.{1}".format(package, name)
+            __import__(package_name, level=0)
+            package_tests = getattr(sys.modules[package_name], 'test_suite')()
+            suite.addTest(package_tests)
+    return suite
+
+
+if __name__ == '__main__':
+    from test.support import run_unittest
+    run_unittest(test_suite('importlib.test'))
diff --git a/Lib/importlib/test/builtin/__init__.py b/Lib/importlib/test/builtin/__init__.py
new file mode 100644 (file)
index 0000000..31a3b5f
--- /dev/null
@@ -0,0 +1,12 @@
+import importlib.test
+import os
+
+
+def test_suite():
+    directory = os.path.dirname(__file__)
+    return importlib.test.test_suite('importlib.test.builtin', directory)
+
+
+if __name__ == '__main__':
+    from test.support import run_unittest
+    run_unittest(test_suite())
diff --git a/Lib/importlib/test/builtin/test_finder.py b/Lib/importlib/test/builtin/test_finder.py
new file mode 100644 (file)
index 0000000..5262aaa
--- /dev/null
@@ -0,0 +1,36 @@
+import importlib
+from .. import support
+
+import sys
+import unittest
+
+class FinderTests(unittest.TestCase):
+
+    """Test find_module() for built-in modules."""
+
+    assert 'errno' in sys.builtin_module_names
+    name = 'errno'
+
+    find_module = staticmethod(lambda name, path=None:
+                    importlib.BuiltinImporter().find_module(name, path))
+
+
+    def test_find_module(self):
+        # Common case.
+        with support.uncache(self.name):
+            self.assert_(self.find_module(self.name))
+
+    def test_ignore_path(self):
+        # The value for 'path' should always trigger a failed import.
+        with support.uncache(self.name):
+            self.assert_(self.find_module(self.name, ['pkg']) is None)
+
+
+
+def test_main():
+    from test.support import run_unittest
+    run_unittest(FinderTests)
+
+
+if __name__ == '__main__':
+    test_main()
diff --git a/Lib/importlib/test/builtin/test_loader.py b/Lib/importlib/test/builtin/test_loader.py
new file mode 100644 (file)
index 0000000..5aa3d79
--- /dev/null
@@ -0,0 +1,52 @@
+import importlib
+from .. import support
+
+import sys
+import types
+import unittest
+
+
+class LoaderTests(unittest.TestCase):
+
+    """Test load_module() for built-in modules."""
+
+    assert 'errno' in sys.builtin_module_names
+    name = 'errno'
+
+    verification = {'__name__': 'errno', '__package__': None}
+
+    def verify(self, module):
+        """Verify that the module matches against what it should have."""
+        self.assert_(isinstance(module, types.ModuleType))
+        for attr, value in self.verification.items():
+            self.assertEqual(getattr(module, attr), value)
+        self.assert_(module.__name__ in sys.modules)
+
+    load_module = staticmethod(lambda name:
+                                importlib.BuiltinImporter().load_module(name))
+
+    def test_load_module(self):
+        # Common case.
+        with support.uncache(self.name):
+            module = self.load_module(self.name)
+            self.verify(module)
+
+    def test_nonexistent(self):
+        name = 'dssdsdfff'
+        assert name not in sys.builtin_module_names
+        self.assertRaises(ImportError, self.load_module, name)
+
+    def test_already_imported(self):
+        # Using the name of a module already imported but not a built-in should
+        # still fail.
+        assert hasattr(importlib, '__file__')  # Not a built-in.
+        self.assertRaises(ImportError, self.load_module, 'importlib')
+
+
+def test_main():
+    from test.support import run_unittest
+    run_unittest(LoaderTests)
+
+
+if __name__ == '__main__':
+    test_main()
diff --git a/Lib/importlib/test/extension/__init__.py b/Lib/importlib/test/extension/__init__.py
new file mode 100644 (file)
index 0000000..2ec5840
--- /dev/null
@@ -0,0 +1,13 @@
+import importlib.test
+import os.path
+import unittest
+
+
+def test_suite():
+    directory = os.path.dirname(__file__)
+    return importlib.test.test_suite('importlib.test.extension', directory)
+
+
+if __name__ == '__main__':
+    from test.support import run_unittest
+    run_unittest(test_suite())
diff --git a/Lib/importlib/test/extension/test_case_sensitivity.py b/Lib/importlib/test/extension/test_case_sensitivity.py
new file mode 100644 (file)
index 0000000..c566b23
--- /dev/null
@@ -0,0 +1,39 @@
+import sys
+from test import support as test_support
+import unittest
+import importlib
+from . import test_path_hook
+
+
+class ExtensionModuleCaseSensitivityTest(unittest.TestCase):
+
+    def find_module(self):
+        good_name = test_path_hook.NAME
+        bad_name = good_name.upper()
+        assert good_name != bad_name
+        finder = importlib.ExtensionFileImporter(test_path_hook.PATH)
+        return finder.find_module(bad_name)
+
+    def test_case_sensitive(self):
+        with test_support.EnvironmentVarGuard() as env:
+            env.unset('PYTHONCASEOK')
+            loader = self.find_module()
+            self.assert_(loader is None)
+
+    def test_case_insensitivity(self):
+        with test_support.EnvironmentVarGuard() as env:
+            env.set('PYTHONCASEOK', '1')
+            loader = self.find_module()
+            self.assert_(hasattr(loader, 'load_module'))
+
+
+
+
+def test_main():
+    if sys.platform not in ('win32', 'darwin', 'cygwin'):
+        return
+    test_support.run_unittest(ExtensionModuleCaseSensitivityTest)
+
+
+if __name__ == '__main__':
+    test_main()
diff --git a/Lib/importlib/test/extension/test_finder.py b/Lib/importlib/test/extension/test_finder.py
new file mode 100644 (file)
index 0000000..5766910
--- /dev/null
@@ -0,0 +1,29 @@
+import importlib
+from . import test_path_hook
+
+import unittest
+
+class FinderTests(unittest.TestCase):
+
+    """Test the finder for extension modules."""
+
+    def find_module(self, fullname):
+        importer = importlib.ExtensionFileImporter(test_path_hook.PATH)
+        return importer.find_module(fullname)
+
+    def test_success(self):
+        self.assert_(self.find_module(test_path_hook.NAME))
+
+    def test_failure(self):
+        self.assert_(self.find_module('asdfjkl;') is None)
+
+    # XXX Raise an exception if someone tries to use the 'path' argument?
+
+
+def test_main():
+    from test.support import run_unittest
+    run_unittest(FinderTests)
+
+
+if __name__ == '__main__':
+    test_main()
diff --git a/Lib/importlib/test/extension/test_loader.py b/Lib/importlib/test/extension/test_loader.py
new file mode 100644 (file)
index 0000000..4f2a0d8
--- /dev/null
@@ -0,0 +1,37 @@
+import importlib
+from . import test_path_hook
+from .. import support
+
+import sys
+import unittest
+
+
+class LoaderTests(unittest.TestCase):
+
+    """Test load_module() for extension modules."""
+
+    def load_module(self, fullname):
+        loader = importlib._ExtensionFileLoader(test_path_hook.NAME,
+                                                test_path_hook.FILEPATH,
+                                                False)
+        return loader.load_module(fullname)
+
+    def test_success(self):
+        with support.uncache(test_path_hook.NAME):
+            module = self.load_module(test_path_hook.NAME)
+            for attr, value in [('__name__', test_path_hook.NAME),
+                                ('__file__', test_path_hook.FILEPATH)]:
+                self.assertEqual(getattr(module, attr), value)
+            self.assert_(test_path_hook.NAME in sys.modules)
+
+    def test_failure(self):
+        self.assertRaises(ImportError, self.load_module, 'asdfjkl;')
+
+
+def test_main():
+    from test.support import run_unittest
+    run_unittest(LoaderTests)
+
+
+if __name__ == '__main__':
+    test_main()
diff --git a/Lib/importlib/test/extension/test_path_hook.py b/Lib/importlib/test/extension/test_path_hook.py
new file mode 100644 (file)
index 0000000..f1774fd
--- /dev/null
@@ -0,0 +1,50 @@
+import importlib
+
+import collections
+import imp
+from os import path
+import sys
+import unittest
+
+
+PATH = None
+EXT = None
+FILENAME = None
+NAME = '_testcapi'
+_file_exts = [x[0] for x in imp.get_suffixes() if x[2] == imp.C_EXTENSION]
+try:
+    for PATH in sys.path:
+        for EXT in _file_exts:
+            FILENAME = NAME + EXT
+            FILEPATH = path.join(PATH, FILENAME)
+            if path.exists(path.join(PATH, FILENAME)):
+                raise StopIteration
+    else:
+        PATH = EXT = FILENAME = FILEPATH = None
+except StopIteration:
+    pass
+del _file_exts
+
+
+class PathHookTests(unittest.TestCase):
+
+    """Test the path hook for extension modules."""
+    # XXX Should it only succeed for pre-existing directories?
+    # XXX Should it only work for directories containing an extension module?
+
+    def hook(self, entry):
+        return importlib.ExtensionFileImporter(entry)
+
+    def test_success(self):
+        # Path hook should handle a directory where a known extension module
+        # exists.
+        self.assert_(hasattr(self.hook(PATH), 'find_module'))
+
+
+def test_main():
+    from test.support import run_unittest
+    run_unittest(PathHookTests)
+
+
+if __name__ == '__main__':
+    test_main()
diff --git a/Lib/importlib/test/finder_tests.py b/Lib/importlib/test/finder_tests.py
new file mode 100644 (file)
index 0000000..9bbf85a
--- /dev/null
@@ -0,0 +1,39 @@
+# top-level.
+# Package.
+# module in pacakge.
+# Package within a package.
+# At least one tests with 'path'.
+# Module that is not handled.
+
+import unittest
+
+
+class FinderTests(unittest.TestCase):
+
+    """Basic tests for a finder to pass."""
+
+    def test_module(self):
+        # Test importing a top-level module.
+        raise NotImplementedError
+
+    def test_package(self):
+        # Test importing a package.
+        raise NotImplementedError
+
+    def test_module_in_package(self):
+        # Test importing a module contained within a package.
+        # A value for 'path' should be used if for a meta_path finder.
+        raise NotImplementedError
+
+    def test_package_in_package(self):
+        # Test importing a subpackage.
+        # A value for 'path' should be used if for a meta_path finder.
+        raise NotImplementedError
+
+    def test_package_over_module(self):
+        # Test that packages are chosen over modules.
+        raise NotImplementedError
+
+    def test_failure(self):
+        # Test trying to find a module that cannot be handled.
+        raise NotImplementedError
diff --git a/Lib/importlib/test/frozen/__init__.py b/Lib/importlib/test/frozen/__init__.py
new file mode 100644 (file)
index 0000000..2945eeb
--- /dev/null
@@ -0,0 +1,13 @@
+import importlib.test
+import os.path
+import unittest
+
+
+def test_suite():
+    directory = os.path.dirname(__file__)
+    return importlib.test.test_suite('importlib.test.frozen', directory)
+
+
+if __name__ == '__main__':
+    from test.support import run_unittest
+    run_unittest(test_suite())
diff --git a/Lib/importlib/test/frozen/support.py b/Lib/importlib/test/frozen/support.py
new file mode 100644 (file)
index 0000000..e08b89e
--- /dev/null
@@ -0,0 +1,24 @@
+import sys
+
+
+class Null:
+
+    """Just absorb what is given."""
+
+    def __getattr__(self):
+        return lambda *args, **kwargs: None
+
+
+class SilenceStdout:
+
+    """Silence sys.stdout."""
+
+    def setUp(self):
+        """Substitute sys.stdout with something that does not print to the
+        screen thanks to what bytecode is frozen."""
+        sys.stdout = Null()
+        super().setUp()
+
+    def tearDown(self):
+        sys.stdout = sys.__stdout__
+        super().tearDown()
diff --git a/Lib/importlib/test/frozen/test_finder.py b/Lib/importlib/test/frozen/test_finder.py
new file mode 100644 (file)
index 0000000..2541019
--- /dev/null
@@ -0,0 +1,44 @@
+import importlib
+from ..builtin import test_finder
+from .. import support
+
+import unittest
+
+
+class FinderTests(test_finder.FinderTests):
+
+    """Test finding frozen modules."""
+
+    def find(self, name, path=None):
+        finder = importlib.FrozenImporter()
+        return finder.find_module(name, path)
+
+
+    def test_module(self):
+        name = '__hello__'
+        loader = self.find(name)
+        self.assert_(hasattr(loader, 'load_module'))
+
+    def test_package(self):
+        loader = self.find('__phello__')
+        self.assert_(hasattr(loader, 'load_module'))
+
+    def test_module_in_package(self):
+        loader = self.find('__phello__.spam', ['__phello__'])
+        self.assert_(hasattr(loader, 'load_module'))
+
+    def test_package_in_package(self):
+        pass
+
+    def test_failure(self):
+        loader = self.find('<not real>')
+        self.assert_(loader is None)
+
+
+def test_main():
+    from test.support import run_unittest
+    run_unittest(FinderTests)
+
+
+if __name__ == '__main__':
+    test_main()
diff --git a/Lib/importlib/test/frozen/test_loader.py b/Lib/importlib/test/frozen/test_loader.py
new file mode 100644 (file)
index 0000000..b37ae7f
--- /dev/null
@@ -0,0 +1,27 @@
+import importlib
+from ..builtin import test_loader
+
+
+class LoaderTests(test_loader.LoaderTests):
+
+    name = '__phello__'
+    load_module = staticmethod(lambda name:
+                                importlib.FrozenImporter().load_module(name))
+    verification = {'__name__': '__phello__', '__file__': '<frozen>',
+                    '__package__': None, '__path__': ['__phello__']}
+
+
+class SubmoduleLoaderTests(LoaderTests):
+
+    name = '__phello__.spam'
+    verification = {'__name__': '__phello__.spam', '__file__': '<frozen>',
+                    '__package__': None}
+
+
+def test_main():
+    from test.support import run_unittest
+    run_unittest(LoaderTests, SubmoduleLoaderTests)
+
+
+if __name__ == '__main__':
+    test_main()
diff --git a/Lib/importlib/test/import_/__init__.py b/Lib/importlib/test/import_/__init__.py
new file mode 100644 (file)
index 0000000..fdf7661
--- /dev/null
@@ -0,0 +1,13 @@
+import importlib.test
+import os.path
+import unittest
+
+
+def test_suite():
+    directory = os.path.dirname(__file__)
+    return importlib.test.test_suite('importlib.test.import_', directory)
+
+
+if __name__ == '__main__':
+    from test.support import run_unittest
+    run_unittest(test_suite())
diff --git a/Lib/importlib/test/import_/test___package__.py b/Lib/importlib/test/import_/test___package__.py
new file mode 100644 (file)
index 0000000..64dab3a
--- /dev/null
@@ -0,0 +1,110 @@
+"""PEP 366 ("Main module explicit relative imports") specifies the
+semantics for the __package__ attribute on modules. This attribute is
+used, when available, to detect which package a module belongs to (instead
+of using the typical __path__/__name__ test).
+
+"""
+import unittest
+from .. import support
+
+
+class Using__package__(unittest.TestCase):
+
+    """Use of __package__ supercedes the use of __name__/__path__ to calculate
+    what package a module belongs to. The basic algorithm is [__package__]::
+
+      def resolve_name(name, package, level):
+          level -= 1
+          base = package.rsplit('.', level)[0]
+          return '{0}.{1}'.format(base, name)
+
+    But since there is no guarantee that __package__ has been set, there has to
+    be a way to calculate the attribute's value [__name__]::
+
+      def calc_package(caller_name, has___path__):
+          if has__path__:
+              return caller_name
+          else:
+              return caller_name.rsplit('.', 1)[0]
+
+    Then the normal algorithm for relative name imports can proceed as if
+    __package__ had been set.
+
+    """
+
+    def test_using___package__(self):
+        # [__package__]
+        with support.mock_modules('pkg.__init__', 'pkg.fake') as importer:
+            with support.import_state(meta_path=[importer]):
+                support.import_('pkg.fake')
+                module = support.import_('', globals={'__package__': 'pkg.fake'},
+                                 fromlist=['attr'], level=2)
+        self.assertEquals(module.__name__, 'pkg')
+
+    def test_using___name__(self):
+        # [__name__]
+        with support.mock_modules('pkg.__init__', 'pkg.fake') as importer:
+            with support.import_state(meta_path=[importer]):
+                support.import_('pkg.fake')
+                module = support.import_('',
+                                 globals={'__name__': 'pkg.fake',
+                                          '__path__': []},
+                                 fromlist=['attr'], level=2)
+            self.assertEquals(module.__name__, 'pkg')
+
+    def test_bad__package__(self):
+        globals = {'__package__': '<not real>'}
+        self.assertRaises(SystemError, support.import_,'', globals, {},
+                            ['relimport'], 1)
+
+    def test_bunk__package__(self):
+        globals = {'__package__': 42}
+        self.assertRaises(ValueError, support.import_, '', globals, {},
+                            ['relimport'], 1)
+
+
+class Setting__package__(unittest.TestCase):
+
+    """Because __package__ is a new feature, it is not always set by a loader.
+    Import will set it as needed to help with the transition to relying on
+    __package__.
+
+    For a top-level module, __package__ is set to None [top-level]. For a
+    package __name__ is used for __package__ [package]. For submodules the
+    value is __name__.rsplit('.', 1)[0] [submodule].
+
+    """
+
+    # [top-level]
+    def test_top_level(self):
+        with support.mock_modules('top_level') as mock:
+            with support.import_state(meta_path=[mock]):
+                del mock['top_level'].__package__
+                module = support.import_('top_level')
+                self.assert_(module.__package__ is None)
+
+    # [package]
+    def test_package(self):
+        with support.mock_modules('pkg.__init__') as mock:
+            with support.import_state(meta_path=[mock]):
+                del mock['pkg'].__package__
+                module = support.import_('pkg')
+                self.assertEqual(module.__package__, 'pkg')
+
+    # [submodule]
+    def test_submodule(self):
+        with support.mock_modules('pkg.__init__', 'pkg.mod') as mock:
+            with support.import_state(meta_path=[mock]):
+                del mock['pkg.mod'].__package__
+                pkg = support.import_('pkg.mod')
+                module = getattr(pkg, 'mod')
+                self.assertEqual(module.__package__, 'pkg')
+
+
+def test_main():
+    from test.support import run_unittest
+    run_unittest(Using__package__, Setting__package__)
+
+
+if __name__ == '__main__':
+    test_main()
diff --git a/Lib/importlib/test/import_/test_caching.py b/Lib/importlib/test/import_/test_caching.py
new file mode 100644 (file)
index 0000000..57690d4
--- /dev/null
@@ -0,0 +1,75 @@
+"""Test that sys.modules is used properly by import."""
+from ..support import import_, mock_modules, importlib_only, import_state
+
+import sys
+from types import MethodType
+import unittest
+
+
+class UseCache(unittest.TestCase):
+
+    """When it comes to sys.modules, import prefers it over anything else.
+
+    Once a name has been resolved, sys.modules is checked to see if it contains
+    the module desired. If so, then it is returned [use cache]. If it is not
+    found, then the proper steps are taken to perform the import, but
+    sys.modules is still used to return the imported module (e.g., not what a
+    loader returns) [from cache on return]. This also applies to imports of
+    things contained within a package and thus get assigned as an attribute
+    [from cache to attribute] or pulled in thanks to a fromlist import
+    [from cache for fromlist].
+
+    """
+    def test_using_cache(self):
+        # [use cache]
+        module_to_use = "some module found!"
+        sys.modules['some_module'] = module_to_use
+        module = import_('some_module')
+        self.assertEqual(id(module_to_use), id(module))
+
+    def create_mock(self, *names, return_=None):
+        mock = mock_modules(*names)
+        original_load = mock.load_module
+        def load_module(self, fullname):
+            original_load(fullname)
+            return return_
+        mock.load_module = MethodType(load_module, mock)
+        return mock
+
+    # __import__ inconsistent between loaders and built-in import when it comes
+    #   to when to use the module in sys.modules and when not to.
+    @importlib_only
+    def test_using_cache_after_loader(self):
+        # [from cache on return]
+        with self.create_mock('module') as mock:
+            with import_state(meta_path=[mock]):
+                module = import_('module')
+                self.assertEquals(id(module), id(sys.modules['module']))
+
+    # See test_using_cache_after_loader() for reasoning.
+    @importlib_only
+    def test_using_cache_for_assigning_to_attribute(self):
+        # [from cache to attribute]
+        with self.create_mock('pkg.__init__', 'pkg.module') as importer:
+            with import_state(meta_path=[importer]):
+                module = import_('pkg.module')
+                self.assert_(hasattr(module, 'module'))
+                self.assert_(id(module.module), id(sys.modules['pkg.module']))
+
+    # See test_using_cache_after_loader() for reasoning.
+    @importlib_only
+    def test_using_cache_for_fromlist(self):
+        # [from cache for fromlist]
+        with self.create_mock('pkg.__init__', 'pkg.module') as importer:
+            with import_state(meta_path=[importer]):
+                module = import_('pkg', fromlist=['module'])
+                self.assert_(hasattr(module, 'module'))
+                self.assertEquals(id(module.module), id(sys.modules['pkg.module']))
+
+
+def test_main():
+    from test.support import run_unittest
+    run_unittest(UseCache)
+
+if __name__ == '__main__':
+    test_main()
diff --git a/Lib/importlib/test/import_/test_fromlist.py b/Lib/importlib/test/import_/test_fromlist.py
new file mode 100644 (file)
index 0000000..884b516
--- /dev/null
@@ -0,0 +1,116 @@
+"""Test that the semantics relating to the 'fromlist' argument are correct."""
+from ..support import import_, mock_modules, import_state
+
+import unittest
+
+class ReturnValue(unittest.TestCase):
+
+    """The use of fromlist influences what import returns.
+
+    If direct ``import ...`` statement is used, the root module or package is
+    returned [import return]. But if fromlist is set, then the specified module
+    is actually returned (whether it is a relative import or not)
+    [from return].
+
+    """
+
+    def test_return_from_import(self):
+        # [import return]
+        with mock_modules('pkg.__init__', 'pkg.module') as importer:
+            with import_state(meta_path=[importer]):
+                module = import_('pkg.module')
+                self.assertEquals(module.__name__, 'pkg')
+
+    def test_return_from_from_import(self):
+        # [from return]
+        with mock_modules('pkg.__init__', 'pkg.module')as importer:
+            with import_state(meta_path=[importer]):
+                module = import_('pkg.module', fromlist=['attr'])
+                self.assertEquals(module.__name__, 'pkg.module')
+
+
+class HandlingFromlist(unittest.TestCase):
+
+    """Using fromlist triggers different actions based on what is being asked
+    of it.
+
+    If fromlist specifies an object on a module, nothing special happens
+    [object case]. This is even true if the object does not exist [bad object].
+
+    If a package is being imported, then what is listed in fromlist may be
+    treated as a module to be imported [module]. But once again, even if
+    something in fromlist does not exist as a module, no error is thrown
+    [no module]. And this extends to what is contained in __all__ when '*' is
+    imported [using *]. And '*' does not need to be the only name in the
+    fromlist [using * with others].
+
+    """
+
+    def test_object(self):
+        # [object case]
+        with mock_modules('module') as importer:
+            with import_state(meta_path=[importer]):
+                module = import_('module', fromlist=['attr'])
+                self.assertEquals(module.__name__, 'module')
+
+    def test_unexistent_object(self):
+        # [bad object]
+        with mock_modules('module') as importer:
+            with import_state(meta_path=[importer]):
+                module = import_('module', fromlist=['non_existent'])
+                self.assertEquals(module.__name__, 'module')
+                self.assert_(not hasattr(module, 'non_existent'))
+
+    def test_module_from_package(self):
+        # [module]
+        with mock_modules('pkg.__init__', 'pkg.module') as importer:
+            with import_state(meta_path=[importer]):
+                module = import_('pkg', fromlist=['module'])
+                self.assertEquals(module.__name__, 'pkg')
+                self.assert_(hasattr(module, 'module'))
+                self.assertEquals(module.module.__name__, 'pkg.module')
+
+    def test_no_module_from_package(self):
+        # [no module]
+        with mock_modules('pkg.__init__') as importer:
+            with import_state(meta_path=[importer]):
+                module = import_('pkg', fromlist='non_existent')
+                self.assertEquals(module.__name__, 'pkg')
+                self.assert_(not hasattr(module, 'non_existent'))
+
+    def test_empty_string(self):
+        with mock_modules('pkg.__init__', 'pkg.mod') as importer:
+            with import_state(meta_path=[importer]):
+                module = import_('pkg.mod', fromlist=[''])
+                self.assertEquals(module.__name__, 'pkg.mod')
+
+    def test_using_star(self):
+        # [using *]
+        with mock_modules('pkg.__init__', 'pkg.module') as mock:
+            with import_state(meta_path=[mock]):
+                mock['pkg'].__all__ = ['module']
+                module = import_('pkg', fromlist=['*'])
+                self.assertEquals(module.__name__, 'pkg')
+                self.assert_(hasattr(module, 'module'))
+                self.assertEqual(module.module.__name__, 'pkg.module')
+
+    def test_star_with_others(self):
+        # [using * with others]
+        context = mock_modules('pkg.__init__', 'pkg.module1', 'pkg.module2')
+        with context as mock:
+            with import_state(meta_path=[mock]):
+                mock['pkg'].__all__ = ['module1']
+                module = import_('pkg', fromlist=['module2', '*'])
+                self.assertEquals(module.__name__, 'pkg')
+                self.assert_(hasattr(module, 'module1'))
+                self.assert_(hasattr(module, 'module2'))
+                self.assertEquals(module.module1.__name__, 'pkg.module1')
+                self.assertEquals(module.module2.__name__, 'pkg.module2')
+
+
+def test_main():
+    from test.support import run_unittest
+    run_unittest(ReturnValue, HandlingFromlist)
+
+if __name__ == '__main__':
+    test_main()
diff --git a/Lib/importlib/test/import_/test_meta_path.py b/Lib/importlib/test/import_/test_meta_path.py
new file mode 100644 (file)
index 0000000..37f40d7
--- /dev/null
@@ -0,0 +1,99 @@
+from ..support import import_state, mock_modules, import_
+
+from contextlib import nested
+from types import MethodType
+import unittest
+
+
+class CallingOrder(unittest.TestCase):
+
+    """Calls to the importers on sys.meta_path happen in order that they are
+    specified in the sequence, starting with the first importer
+    [first called], and then continuing on down until one is found that doesn't
+    return None [continuing]."""
+
+
+    def test_first_called(self):
+        # [first called]
+        mod = 'top_level'
+        first = mock_modules(mod)
+        second = mock_modules(mod)
+        with nested(mock_modules(mod), mock_modules(mod)) as (first, second):
+            first.modules[mod] = 42
+            second.modules[mod] = -13
+            with import_state(meta_path=[first, second]):
+                self.assertEquals(import_(mod), 42)
+
+    def test_continuing(self):
+        # [continuing]
+        mod_name = 'for_real'
+        first = mock_modules('nonexistent')
+        second = mock_modules(mod_name)
+        with nested(first, second):
+            first.find_module = lambda self, fullname, path=None: None
+            second.modules[mod_name] = 42
+            with import_state(meta_path=[first, second]):
+                self.assertEquals(import_(mod_name), 42)
+
+
+class CallSignature(unittest.TestCase):
+
+    """If there is no __path__ entry on the parent module, then 'path' is None
+    [no path]. Otherwise, the value for __path__ is passed in for the 'path'
+    argument [path set]."""
+
+    def log(self, fxn):
+        log = []
+        def wrapper(self, *args, **kwargs):
+            log.append([args, kwargs])
+            return fxn(*args, **kwargs)
+        return log, wrapper
+
+
+    def test_no_path(self):
+        # [no path]
+        mod_name = 'top_level'
+        assert '.' not in mod_name
+        with mock_modules(mod_name) as importer:
+            log, wrapped_call = self.log(importer.find_module)
+            importer.find_module = MethodType(wrapped_call, importer)
+            with import_state(meta_path=[importer]):
+                import_(mod_name)
+                assert len(log) == 1
+                args = log[0][0]
+                kwargs = log[0][1]
+                # Assuming all arguments are positional.
+                self.assertEquals(len(args), 2)
+                self.assertEquals(len(kwargs), 0)
+                self.assertEquals(args[0], mod_name)
+                self.assert_(args[1] is None)
+
+    def test_with_path(self):
+        # [path set]
+        pkg_name = 'pkg'
+        mod_name = pkg_name + '.module'
+        path = [42]
+        assert '.' in mod_name
+        with mock_modules(pkg_name+'.__init__', mod_name) as importer:
+            importer.modules[pkg_name].__path__ = path
+            log, wrapped_call = self.log(importer.find_module)
+            importer.find_module = MethodType(wrapped_call, importer)
+            with import_state(meta_path=[importer]):
+                import_(mod_name)
+                assert len(log) == 2
+                args = log[1][0]
+                kwargs = log[1][1]
+                # Assuming all arguments are positional.
+                self.assert_(not kwargs)
+                self.assertEquals(args[0], mod_name)
+                self.assert_(args[1] is path)
+
+
+
+def test_main():
+    from test.support import run_unittest
+    run_unittest(CallingOrder, CallSignature)
+
+
+if __name__ == '__main__':
+    test_main()
diff --git a/Lib/importlib/test/import_/test_packages.py b/Lib/importlib/test/import_/test_packages.py
new file mode 100644 (file)
index 0000000..013bbdc
--- /dev/null
@@ -0,0 +1,29 @@
+import sys
+import unittest
+import importlib
+from .. import support
+
+
+class ParentModuleTests(unittest.TestCase):
+
+    """Importing a submodule should import the parent modules."""
+
+    def test_import_parent(self):
+        with support.mock_modules('pkg.__init__', 'pkg.module') as mock:
+            with support.import_state(meta_path=[mock]):
+                module = support.import_('pkg.module')
+                self.assert_('pkg' in sys.modules)
+
+    def test_bad_parent(self):
+        with support.mock_modules('pkg.module') as mock:
+            with support.import_state(meta_path=[mock]):
+                self.assertRaises(ImportError, support.import_, 'pkg.module')
+
+
+def test_main():
+    from test.support import run_unittest
+    run_unittest(ParentModuleTests)
+
+
+if __name__ == '__main__':
+    test_main()
diff --git a/Lib/importlib/test/import_/test_path.py b/Lib/importlib/test/import_/test_path.py
new file mode 100644 (file)
index 0000000..c939907
--- /dev/null
@@ -0,0 +1,158 @@
+from ..support import (mock_modules, import_state, import_, mock_path_hook,
+                        importlib_only, uncache)
+
+from contextlib import nested
+from imp import new_module
+import sys
+from types import MethodType
+import unittest
+
+
+class BaseTests(unittest.TestCase):
+
+    """When sys.meta_path cannot find the desired module, sys.path is
+    consulted. For each entry on the sequence [order], sys.path_importer_cache
+    is checked to see if it contains a key for the entry [cache check]. If an
+    importer is found then it is consulted before trying the next entry in
+    sys.path [cache use]. The 'path' argument to find_module() is never used
+    when trying to find a module [path not used].
+
+    If an entry from sys.path is not in sys.path_importer_cache, sys.path_hooks
+    is called in turn [hooks order]. If a path hook cannot handle an entry,
+    ImportError is raised [hook failure]. Otherwise the resulting object is
+    cached in sys.path_importer_cache and then consulted [hook success]. If no
+    hook is found, None is set in sys.path_importer_cache and the default
+    importer is tried [no hook].
+
+    For use of __path__ in a package, the above is all true, just substitute
+    "sys.path" for "__path__".
+
+    """
+
+    def order_test(self, to_import, entry, search_path, path=[]):
+        # [order]
+        log = []
+        class LogFindModule(mock_modules):
+            def find_module(self, fullname):
+                log.append(self)
+                return super().find_module(fullname)
+
+        assert len(search_path) == 2
+        misser = LogFindModule(search_path[0])
+        hitter = LogFindModule(to_import)
+        with nested(misser, hitter):
+            cache = dict(zip(search_path, (misser, hitter)))
+            with import_state(path=path, path_importer_cache=cache):
+                import_(to_import)
+        self.assertEquals(log[0], misser)
+        self.assertEquals(log[1], hitter)
+
+    @importlib_only  # __import__ uses PyDict_GetItem(), bypassing log.
+    def cache_use_test(self, to_import, entry, path=[]):
+        # [cache check], [cache use]
+        log = []
+        class LoggingDict(dict):
+            def __getitem__(self, item):
+                log.append(item)
+                return super(LoggingDict, self).__getitem__(item)
+
+        with mock_modules(to_import) as importer:
+            cache = LoggingDict()
+            cache[entry] = importer
+            with import_state(path=[entry], path_importer_cache=cache):
+                module = import_(to_import, fromlist=['a'])
+            self.assert_(module is importer[to_import])
+        self.assertEquals(len(cache), 1)
+        self.assertEquals([entry], log)
+
+    def hooks_order_test(self, to_import, entry, path=[]):
+        # [hooks order], [hooks failure], [hook success]
+        log = []
+        def logging_hook(entry):
+            log.append(entry)
+            raise ImportError
+        with mock_modules(to_import) as importer:
+            hitter = mock_path_hook(entry, importer=importer)
+            path_hooks = [logging_hook, logging_hook, hitter]
+            with import_state(path_hooks=path_hooks, path=path):
+                import_(to_import)
+                self.assertEquals(sys.path_importer_cache[entry], importer)
+        self.assertEquals(len(log), 2)
+
+    # [no hook] XXX Worry about after deciding how to handle the default hook.
+
+    def path_argument_test(self, to_import):
+        # [path not used]
+        class BadImporter:
+            """Class to help detect TypeError from calling find_module() with
+            an improper number of arguments."""
+            def find_module(name):
+                raise ImportError
+
+        try:
+            import_(to_import)
+        except ImportError:
+            pass
+
+
+class PathTests(BaseTests):
+
+    """Tests for sys.path."""
+
+    def test_order(self):
+        self.order_test('hit', 'second', ['first', 'second'],
+                        ['first', 'second'])
+
+    def test_cache_use(self):
+        entry = "found!"
+        self.cache_use_test('hit', entry, [entry])
+
+    def test_hooks_order(self):
+        entry = "found!"
+        self.hooks_order_test('hit', entry, [entry])
+
+    def test_path_argument(self):
+        name = 'total junk'
+        with uncache(name):
+            self.path_argument_test(name)
+
+
+class __path__Tests(BaseTests):
+
+    """Tests for __path__."""
+
+    def run_test(self, test, entry, path, *args):
+        with mock_modules('pkg.__init__') as importer:
+            importer['pkg'].__path__ = path
+            importer.load_module('pkg')
+            test('pkg.hit', entry, *args)
+
+
+    @importlib_only  # XXX Unknown reason why this fails.
+    def test_order(self):
+        self.run_test(self.order_test, 'second', ('first', 'second'), ['first',
+            'second'])
+
+    def test_cache_use(self):
+        location = "I'm here!"
+        self.run_test(self.cache_use_test, location, [location])
+
+    def test_hooks_order(self):
+        location = "I'm here!"
+        self.run_test(self.hooks_order_test, location, [location])
+
+    def test_path_argument(self):
+        module = new_module('pkg')
+        module.__path__ = ['random __path__']
+        name = 'pkg.whatever'
+        sys.modules['pkg'] = module
+        with uncache('pkg', name):
+            self.path_argument_test(name)
+
+
+def test_main():
+    from test.support import run_unittest
+    run_unittest(PathTests, __path__Tests)
+
+if __name__ == '__main__':
+    test_main()
diff --git a/Lib/importlib/test/import_/test_relative_imports.py b/Lib/importlib/test/import_/test_relative_imports.py
new file mode 100644 (file)
index 0000000..73ef530
--- /dev/null
@@ -0,0 +1,199 @@
+"""Test relative imports (PEP 328)."""
+
+from ..support import uncache, import_, mock_modules, import_state
+
+import sys
+import unittest
+
+class RelativeImports(unittest.TestCase):
+
+    """PEP 328 introduced relative imports. This allows for imports to occur
+    from within a package without having to specify the actual package name.
+
+    A simple example is to import another module within the same package
+    [module from module]::
+
+      # From pkg.mod1 with pkg.mod2 being a module.
+      from . import mod2
+
+    This also works for getting an attribute from a module that is specified
+    in a relative fashion [attr from module]::
+
+      # From pkg.mod1.
+      from .mod2 import attr
+
+    But this is in no way restricted to working between modules; it works
+    from [package to module],::
+
+      # From pkg, importing pkg.module which is a module.
+      from . import module
+
+    [module to package],::
+
+      # Pull attr from pkg, called from pkg.module which is a module.
+      from . import attr
+
+    and [package to package]::
+
+      # From pkg.subpkg1 (both pkg.subpkg[1,2] are packages).
+      from .. import subpkg2
+
+    The number of dots used is in no way restricted [deep import]::
+
+      # Import pkg.attr from pkg.pkg1.pkg2.pkg3.pkg4.pkg5.
+      from ...... import attr
+
+    To prevent someone from accessing code that is outside of a package, one
+    cannot reach the location containing the root package itself::
+
+      # From pkg.__init__ [too high from package]
+      from .. import top_level
+
+      # From pkg.module [too high from module]
+      from .. import top_level
+
+     Relative imports are the only type of import that allow for an empty
+     module name for an import [empty name].
+
+    """
+
+    def relative_import_test(self, create, globals_, callback):
+        """Abstract out boilerplace for setting up for an import test."""
+        uncache_names = []
+        for name in create:
+            if not name.endswith('.__init__'):
+                uncache_names.append(name)
+            else:
+                uncache_names.append(name[:-len('.__init__')])
+        with mock_modules(*create) as importer:
+            with import_state(meta_path=[importer]):
+                for global_ in globals_:
+                    with uncache(*uncache_names):
+                        callback(global_)
+
+
+    def test_module_from_module(self):
+        # [module from module]
+        create = 'pkg.__init__', 'pkg.mod2'
+        globals_ = {'__package__': 'pkg'}, {'__name__': 'pkg.mod1'}
+        def callback(global_):
+            import_('pkg')  # For __import__().
+            module = import_('', global_, fromlist=['mod2'], level=1)
+            self.assertEqual(module.__name__, 'pkg')
+            self.assert_(hasattr(module, 'mod2'))
+            self.assertEqual(module.mod2.attr, 'pkg.mod2')
+        self.relative_import_test(create, globals_, callback)
+
+    def test_attr_from_module(self):
+        # [attr from module]
+        create = 'pkg.__init__', 'pkg.mod2'
+        globals_ = {'__package__': 'pkg'}, {'__name__': 'pkg.mod1'}
+        def callback(global_):
+            import_('pkg')  # For __import__().
+            module = import_('mod2', global_, fromlist=['attr'], level=1)
+            self.assertEqual(module.__name__, 'pkg.mod2')
+            self.assertEqual(module.attr, 'pkg.mod2')
+        self.relative_import_test(create, globals_, callback)
+
+    def test_package_to_module(self):
+        # [package to module]
+        create = 'pkg.__init__', 'pkg.module'
+        globals_ = ({'__package__': 'pkg'},
+                    {'__name__': 'pkg', '__path__': ['blah']})
+        def callback(global_):
+            import_('pkg')  # For __import__().
+            module = import_('', global_, fromlist=['module'],
+                             level=1)
+            self.assertEqual(module.__name__, 'pkg')
+            self.assert_(hasattr(module, 'module'))
+            self.assertEqual(module.module.attr, 'pkg.module')
+        self.relative_import_test(create, globals_, callback)
+
+    def test_module_to_package(self):
+        # [module to package]
+        create = 'pkg.__init__', 'pkg.module'
+        globals_ = {'__package__': 'pkg'}, {'__name__': 'pkg.module'}
+        def callback(global_):
+            import_('pkg')  # For __import__().
+            module = import_('', global_, fromlist=['attr'], level=1)
+            self.assertEqual(module.__name__, 'pkg')
+        self.relative_import_test(create, globals_, callback)
+
+    def test_package_to_package(self):
+        # [package to package]
+        create = ('pkg.__init__', 'pkg.subpkg1.__init__',
+                    'pkg.subpkg2.__init__')
+        globals_ =  ({'__package__': 'pkg.subpkg1'},
+                     {'__name__': 'pkg.subpkg1', '__path__': ['blah']})
+        def callback(global_):
+            module = import_('', global_, fromlist=['subpkg2'], level=2)
+            self.assertEqual(module.__name__, 'pkg')
+            self.assert_(hasattr(module, 'subpkg2'))
+            self.assertEqual(module.subpkg2.attr, 'pkg.subpkg2.__init__')
+
+    def test_deep_import(self):
+        # [deep import]
+        create = ['pkg.__init__']
+        for count in range(1,6):
+            create.append('{0}.pkg{1}.__init__'.format(
+                            create[-1][:-len('.__init__')], count))
+        globals_ = ({'__package__': 'pkg.pkg1.pkg2.pkg3.pkg4.pkg5'},
+                    {'__name__': 'pkg.pkg1.pkg2.pkg3.pkg4.pkg5',
+                        '__path__': ['blah']})
+        def callback(global_):
+            import_(globals_[0]['__package__'])
+            module = import_('', global_, fromlist=['attr'], level=6)
+            self.assertEqual(module.__name__, 'pkg')
+        self.relative_import_test(create, globals_, callback)
+
+    def test_too_high_from_package(self):
+        # [too high from package]
+        create = ['top_level', 'pkg.__init__']
+        globals_ = ({'__package__': 'pkg'},
+                    {'__name__': 'pkg', '__path__': ['blah']})
+        def callback(global_):
+            import_('pkg')
+            self.assertRaises(ValueError, import_, '', global_,
+                                fromlist=['top_level'], level=2)
+        self.relative_import_test(create, globals_, callback)
+
+    def test_too_high_from_module(self):
+        # [too high from module]
+        create = ['top_level', 'pkg.__init__', 'pkg.module']
+        globals_ = {'__package__': 'pkg'}, {'__name__': 'pkg.module'}
+        def callback(global_):
+            import_('pkg')
+            self.assertRaises(ValueError, import_, '', global_,
+                                fromlist=['top_level'], level=2)
+        self.relative_import_test(create, globals_, callback)
+
+    def test_empty_name_w_level_0(self):
+        # [empty name]
+        self.assertRaises(ValueError, import_, '')
+
+    def test_import_from_different_package(self):
+        # Test importing from a different package than the caller.
+        # in pkg.subpkg1.mod
+        # from ..subpkg2 import mod
+        # XXX
+        create = ['__runpy_pkg__.__init__',
+                    '__runpy_pkg__.__runpy_pkg__.__init__',
+                    '__runpy_pkg__.uncle.__init__',
+                    '__runpy_pkg__.uncle.cousin.__init__',
+                    '__runpy_pkg__.uncle.cousin.nephew']
+        globals_ = {'__package__': '__runpy_pkg__.__runpy_pkg__'}
+        def callback(global_):
+            import_('__runpy_pkg__.__runpy_pkg__')
+            module = import_('uncle.cousin', globals_, {}, fromlist=['nephew'],
+                                level=2)
+            self.assertEqual(module.__name__, '__runpy_pkg__.uncle.cousin')
+        self.relative_import_test(create, globals_, callback)
+
+
+
+def test_main():
+    from test.support import run_unittest
+    run_unittest(RelativeImports)
+
+if __name__ == '__main__':
+    test_main()
diff --git a/Lib/importlib/test/source/__init__.py b/Lib/importlib/test/source/__init__.py
new file mode 100644 (file)
index 0000000..8d7c49d
--- /dev/null
@@ -0,0 +1,13 @@
+import importlib.test
+import os.path
+import unittest
+
+
+def test_suite():
+    directory = os.path.dirname(__file__)
+    return importlib.test.test_suite('importlib.test.source', directory)
+
+
+if __name__ == '__main__':
+    from test.support import run_unittest
+    run_unittest(test_suite())
diff --git a/Lib/importlib/test/source/test_case_sensitivity.py b/Lib/importlib/test/source/test_case_sensitivity.py
new file mode 100644 (file)
index 0000000..1a5ff2f
--- /dev/null
@@ -0,0 +1,57 @@
+"""Test case-sensitivity (PEP 235)."""
+import importlib
+from .. import support
+import os
+import sys
+from test import support as test_support
+import unittest
+
+
+class CaseSensitivityTest(unittest.TestCase):
+
+    """PEP 235 dictates that on case-preserving, case-insensitive file systems
+    that imports are case-sensitive unless the PYTHONCASEOK environment
+    variable is set."""
+
+    name = 'MoDuLe'
+    assert name != name.lower()
+
+    def find(self, path):
+        finder = importlib.PyFileImporter(path)
+        return finder.find_module(self.name)
+
+    def sensitivity_test(self):
+        """Look for a module with matching and non-matching sensitivity."""
+        sensitive_pkg = 'sensitive.{0}'.format(self.name)
+        insensitive_pkg = 'insensitive.{0}'.format(self.name.lower())
+        with support.create_modules(insensitive_pkg, sensitive_pkg) as mapping:
+            sensitive_path = os.path.join(mapping['.root'], 'sensitive')
+            insensitive_path = os.path.join(mapping['.root'], 'insensitive')
+            return self.find(sensitive_path), self.find(insensitive_path)
+
+    def test_sensitive(self):
+        with test_support.EnvironmentVarGuard() as env:
+            env.unset('PYTHONCASEOK')
+            sensitive, insensitive = self.sensitivity_test()
+            self.assert_(hasattr(sensitive, 'load_module'))
+            self.assert_(self.name in sensitive._base_path)
+            self.assert_(insensitive is None)
+
+    def test_insensitive(self):
+        with test_support.EnvironmentVarGuard() as env:
+            env.set('PYTHONCASEOK', '1')
+            sensitive, insensitive = self.sensitivity_test()
+            self.assert_(hasattr(sensitive, 'load_module'))
+            self.assert_(self.name in sensitive._base_path)
+            self.assert_(hasattr(insensitive, 'load_module'))
+            self.assert_(self.name in insensitive._base_path)
+
+
+def test_main():
+    if sys.platform not in ('win32', 'darwin', 'cygwin'):
+        return
+    test_support.run_unittest(CaseSensitivityTest)
+
+
+if __name__ == '__main__':
+    test_main()
diff --git a/Lib/importlib/test/source/test_finder.py b/Lib/importlib/test/source/test_finder.py
new file mode 100644 (file)
index 0000000..cf80799
--- /dev/null
@@ -0,0 +1,130 @@
+import importlib
+from .. import finder_tests
+from .. import support
+import os
+import py_compile
+import unittest
+import warnings
+
+
+class FinderTests(finder_tests.FinderTests):
+
+    """For a top-level module, it should just be found directly in the
+    directory being searched. This is true for a directory with source
+    [top-level source], bytecode [top-level bc], or both [top-level both].
+    There is also the possibility that it is a package [top-level package], in
+    which case there will be a directory with the module name and an
+    __init__.py file. If there is a directory without an __init__.py an
+    ImportWarning is returned [empty dir].
+
+    For sub-modules and sub-packages, the same happens as above but only use
+    the tail end of the name [sub module] [sub package] [sub empty].
+
+    When there is a conflict between a package and module having the same name
+    in the same directory, the package wins out [package over module]. This is
+    so that imports of modules within the package can occur rather than trigger
+    an import error.
+
+    When there is a package and module with the same name, always pick the
+    package over the module [package over module]. This is so that imports from
+    the package have the possibility of succeeding.
+
+    """
+
+    def import_(self, root, module):
+        finder = importlib.PyFileImporter(root)
+        return finder.find_module(module)
+
+    def run_test(self, test, create=None, *, compile_=None, unlink=None):
+        """Test the finding of 'test' with the creation of modules listed in
+        'create'.
+
+        Any names listed in 'compile_' are byte-compiled. Modules
+        listed in 'unlink' have their source files deleted.
+
+        """
+        if create is None:
+            create = {test}
+        with support.create_modules(*create) as mapping:
+            if compile_:
+                for name in compile_:
+                    py_compile.compile(mapping[name])
+            if unlink:
+                for name in unlink:
+                    os.unlink(mapping[name])
+            loader = self.import_(mapping['.root'], test)
+            self.assert_(hasattr(loader, 'load_module'))
+            return loader
+
+    def test_module(self):
+        # [top-level source]
+        self.run_test('top_level')
+        # [top-level bc]
+        self.run_test('top_level', compile_={'top_level'}, unlink={'top_level'})
+        # [top-level both]
+        self.run_test('top_level', compile_={'top_level'})
+
+    # [top-level package]
+    def test_package(self):
+        # Source.
+        self.run_test('pkg', {'pkg.__init__'})
+        # Bytecode.
+        self.run_test('pkg', {'pkg.__init__'}, compile_={'pkg.__init__'},
+                unlink={'pkg.__init__'})
+        # Both.
+        self.run_test('pkg', {'pkg.__init__'}, compile_={'pkg.__init__'})
+
+    # [sub module]
+    def test_module_in_package(self):
+        with support.create_modules('pkg.__init__', 'pkg.sub') as mapping:
+            pkg_dir = os.path.dirname(mapping['pkg.__init__'])
+            loader = self.import_(pkg_dir, 'pkg.sub')
+            self.assert_(hasattr(loader, 'load_module'))
+
+    # [sub package]
+    def test_package_in_package(self):
+        context = support.create_modules('pkg.__init__', 'pkg.sub.__init__')
+        with context as mapping:
+            pkg_dir = os.path.dirname(mapping['pkg.__init__'])
+            loader = self.import_(pkg_dir, 'pkg.sub')
+            self.assert_(hasattr(loader, 'load_module'))
+
+    # [sub empty]
+    def test_empty_sub_directory(self):
+        context = support.create_modules('pkg.__init__', 'pkg.sub.__init__')
+        with warnings.catch_warnings():
+            warnings.simplefilter("error", ImportWarning)
+            with context as mapping:
+                os.unlink(mapping['pkg.sub.__init__'])
+                pkg_dir = os.path.dirname(mapping['pkg.__init__'])
+                self.assertRaises(ImportWarning, self.import_, pkg_dir,
+                                    'pkg.sub')
+
+    # [package over modules]
+    def test_package_over_module(self):
+        # XXX This is not a blackbox test!
+        name = '_temp'
+        loader = self.run_test(name, {'{0}.__init__'.format(name), name})
+        self.assert_('__init__' in loader._base_path)
+
+
+    def test_failure(self):
+        with support.create_modules('blah') as mapping:
+            nothing = self.import_(mapping['.root'], 'sdfsadsadf')
+            self.assert_(nothing is None)
+
+    # [empty dir]
+    def test_empty_dir(self):
+        with warnings.catch_warnings():
+            warnings.simplefilter("error", ImportWarning)
+            self.assertRaises(ImportWarning, self.run_test, 'pkg',
+            {'pkg.__init__'}, unlink={'pkg.__init__'})
+
+
+def test_main():
+    from test.support import run_unittest
+    run_unittest(FinderTests)
+
+
+if __name__ == '__main__':
+    test_main()
diff --git a/Lib/importlib/test/source/test_loader.py b/Lib/importlib/test/source/test_loader.py
new file mode 100644 (file)
index 0000000..249bdc0
--- /dev/null
@@ -0,0 +1,201 @@
+import importlib
+from .. import support
+
+import imp
+import os
+import py_compile
+import sys
+import unittest
+
+
+class SimpleTest(unittest.TestCase):
+
+    """Should have no issue importing a source module [basic]. And if there is
+    a syntax error, it should raise a SyntaxError [syntax error].
+
+    """
+
+    # [basic]
+    def test_basic(self):
+        with support.create_modules('_temp') as mapping:
+            loader = importlib._PyFileLoader('_temp', mapping['_temp'], False)
+            loader.load_module('_temp')
+            self.assert_('_temp' in sys.modules)
+
+    # [syntax error]
+    def test_bad_syntax(self):
+        with support.create_modules('_temp') as mapping:
+            with open(mapping['_temp'], 'w') as file:
+                file.write('=')
+            loader = importlib._PyFileLoader('_temp', mapping['_temp'], False)
+            self.assertRaises(SyntaxError, loader.load_module, '_temp')
+            self.assert_('_temp' not in sys.modules)
+
+
+class DontWriteBytecodeTest(unittest.TestCase):
+
+    """If sys.dont_write_bytcode is true then no bytecode should be created."""
+
+    def tearDown(self):
+        sys.dont_write_bytecode = False
+
+    @support.writes_bytecode
+    def run_test(self, assertion):
+        with support.create_modules('_temp') as mapping:
+            loader = importlib._PyFileLoader('_temp', mapping['_temp'], False)
+            loader.load_module('_temp')
+            bytecode_path = support.bytecode_path(mapping['_temp'])
+            assertion(bytecode_path)
+
+    def test_bytecode_written(self):
+        fxn = lambda bc_path: self.assert_(os.path.exists(bc_path))
+        self.run_test(fxn)
+
+    def test_bytecode_not_written(self):
+        sys.dont_write_bytecode = True
+        fxn = lambda bc_path: self.assert_(not os.path.exists(bc_path))
+        self.run_test(fxn)
+
+
+class BadDataTest(unittest.TestCase):
+
+    """If the bytecode has a magic number that does not match the
+    interpreters', ImportError is raised [bad magic]. The timestamp can have
+    any value. And bad marshal data raises ValueError.
+
+    """
+
+    # [bad magic]
+    def test_bad_magic(self):
+        with support.create_modules('_temp') as mapping:
+            py_compile.compile(mapping['_temp'])
+            os.unlink(mapping['_temp'])
+            bytecode_path = support.bytecode_path(mapping['_temp'])
+            with open(bytecode_path, 'r+b') as file:
+                file.seek(0)
+                file.write(b'\x00\x00\x00\x00')
+            loader = importlib._PyFileLoader('_temp', mapping['_temp'], False)
+            self.assertRaises(ImportError, loader.load_module, '_temp')
+            self.assert_('_temp' not in sys.modules)
+
+
+class SourceBytecodeInteraction(unittest.TestCase):
+
+    """When both source and bytecode are present, certain rules dictate which
+    version of the code takes precedent. All things being equal, the bytecode
+    is used with the value of __file__ set to the source [basic top-level],
+    [basic package], [basic sub-module], [basic sub-package].
+
+    """
+
+    def import_(self, file, module, *, pkg=False):
+        loader = importlib._PyFileLoader(module, file, pkg)
+        return loader.load_module(module)
+
+    def run_test(self, test, *create, pkg=False):
+        create += (test,)
+        with support.create_modules(*create) as mapping:
+            for name in create:
+                py_compile.compile(mapping[name])
+            if pkg:
+                import_name = test.rsplit('.', 1)[0]
+            else:
+                import_name = test
+            loader = importlib._PyFileLoader(import_name, mapping[test], pkg)
+            # Because some platforms only have a granularity to the second for
+            # atime you can't check the physical files. Instead just make it an
+            # exception trigger if source was read.
+            loader.get_source = lambda self, x: 42
+            module = loader.load_module(import_name)
+            self.assertEqual(module.__file__, mapping[name])
+            self.assert_(import_name in sys.modules)
+            self.assertEqual(id(module), id(sys.modules[import_name]))
+
+    # [basic top-level]
+    def test_basic_top_level(self):
+        self.run_test('top_level')
+
+    # [basic package]
+    def test_basic_package(self):
+        self.run_test('pkg.__init__', pkg=True)
+
+    # [basic sub-module]
+    def test_basic_sub_module(self):
+        self.run_test('pkg.sub', 'pkg.__init__')
+
+    # [basic sub-package]
+    def test_basic_sub_package(self):
+        self.run_test('pkg.sub.__init__', 'pkg.__init__', pkg=True)
+
+
+class BadBytecodeTest(unittest.TestCase):
+
+    """But there are several things about the bytecode which might lead to the
+    source being preferred. If the magic number differs from what the
+    interpreter uses, then the source is used with the bytecode regenerated.
+    If the timestamp is older than the modification time for the source then
+    the bytecode is not used [bad timestamp].
+
+    But if the marshal data is bad, even if the magic number and timestamp
+    work, a ValueError is raised and the source is not used [bad marshal].
+
+    """
+
+    def import_(self, file, module_name):
+        loader = importlib._PyFileLoader(module_name, file, False)
+        module = loader.load_module(module_name)
+        self.assert_(module_name in sys.modules)
+
+    # [bad magic]
+    @support.writes_bytecode
+    def test_bad_magic(self):
+        with support.create_modules('_temp') as mapping:
+            py_compile.compile(mapping['_temp'])
+            bytecode_path = support.bytecode_path(mapping['_temp'])
+            with open(bytecode_path, 'r+b') as bytecode_file:
+                bytecode_file.seek(0)
+                bytecode_file.write(b'\x00\x00\x00\x00')
+            self.import_(mapping['_temp'], '_temp')
+            with open(bytecode_path, 'rb') as bytecode_file:
+                self.assertEqual(bytecode_file.read(4), imp.get_magic())
+
+    # [bad timestamp]
+    @support.writes_bytecode
+    def test_bad_bytecode(self):
+        zeros = b'\x00\x00\x00\x00'
+        with support.create_modules('_temp') as mapping:
+            py_compile.compile(mapping['_temp'])
+            bytecode_path = support.bytecode_path(mapping['_temp'])
+            with open(bytecode_path, 'r+b') as bytecode_file:
+                bytecode_file.seek(4)
+                bytecode_file.write(zeros)
+            self.import_(mapping['_temp'], '_temp')
+            source_mtime = os.path.getmtime(mapping['_temp'])
+            source_timestamp = importlib._w_long(source_mtime)
+            with open(bytecode_path, 'rb') as bytecode_file:
+                bytecode_file.seek(4)
+                self.assertEqual(bytecode_file.read(4), source_timestamp)
+
+    # [bad marshal]
+    def test_bad_marshal(self):
+        with support.create_modules('_temp') as mapping:
+            bytecode_path = support.bytecode_path(mapping['_temp'])
+            source_mtime = os.path.getmtime(mapping['_temp'])
+            source_timestamp = importlib._w_long(source_mtime)
+            with open(bytecode_path, 'wb') as bytecode_file:
+                bytecode_file.write(imp.get_magic())
+                bytecode_file.write(source_timestamp)
+                bytecode_file.write(b'AAAA')
+            self.assertRaises(ValueError, self.import_, mapping['_temp'],
+                                '_temp')
+            self.assert_('_temp' not in sys.modules)
+
+
+def test_main():
+    from test.support import run_unittest
+    run_unittest(SimpleTest, DontWriteBytecodeTest, BadDataTest,
+                 SourceBytecodeInteraction, BadBytecodeTest)
+
+
+if __name__ == '__main__':
+    test_main()
diff --git a/Lib/importlib/test/source/test_path_hook.py b/Lib/importlib/test/source/test_path_hook.py
new file mode 100644 (file)
index 0000000..ef41077
--- /dev/null
@@ -0,0 +1,23 @@
+import importlib
+from .. import support
+import unittest
+
+
+class PathHookTest(unittest.TestCase):
+
+    """Test the path hook for source."""
+
+    def test_success(self):
+        # XXX Only work on existing directories?
+        with support.create_modules('dummy') as mapping:
+            self.assert_(hasattr(importlib.FileImporter(mapping['.root']),
+                                 'find_module'))
+
+
+def test_main():
+    from test.support import run_unittest
+    run_unittest(PathHookTest)
+
+
+if __name__ == '__main__':
+    test_main()
diff --git a/Lib/importlib/test/source/test_reload.py b/Lib/importlib/test/source/test_reload.py
new file mode 100644 (file)
index 0000000..e123a28
--- /dev/null
@@ -0,0 +1,71 @@
+"""Test reload support.
+
+Reload support requires two things. One is that if module is loaded that
+already exists in sys.modules then it is reused. And two, if a reload fails the
+pre-existing module is left in a sane state.
+
+"""
+import imp
+import sys
+import types
+import unittest
+import importlib
+from .. import support
+
+
+class ReloadTests(unittest.TestCase):
+
+    name = '_temp'
+
+    def load_module(self, mapping):
+        return importlib._PyFileLoader(self.name, mapping[self.name], False)
+
+    def fake_mtime(self, fxn):
+        """Fake mtime to always be higher than expected."""
+        return lambda name: fxn(name) + 1
+
+    def test_module_reuse(self):
+        with support.create_modules(self.name) as mapping:
+            loader = self.load_module(mapping)
+            module = loader.load_module(self.name)
+            module_id = id(module)
+            module_dict_id = id(module.__dict__)
+            with open(mapping[self.name], 'w') as file:
+                file.write("testing_var = 42\n")
+            # For filesystems where the mtime is only to a second granularity,
+            # everything that has happened above can be too fast;
+            # force an mtime on the source that is guaranteed to be different
+            # than the original mtime.
+            loader.source_mtime = self.fake_mtime(loader.source_mtime)
+            module = loader.load_module(self.name)
+            self.assert_('testing_var' in module.__dict__,
+                         "'testing_var' not in "
+                            "{0}".format(list(module.__dict__.keys())))
+            self.assertEqual(module, sys.modules[self.name])
+            self.assertEqual(id(module), module_id)
+            self.assertEqual(id(module.__dict__), module_dict_id)
+
+    def test_bad_reload(self):
+        # A failed reload should leave the original module intact.
+        attributes = ('__file__', '__path__', '__package__')
+        value = '<test>'
+        with support.create_modules(self.name) as mapping:
+            orig_module = imp.new_module(self.name)
+            for attr in attributes:
+                setattr(orig_module, attr, value)
+            with open(mapping[self.name], 'w') as file:
+                file.write('+++ bad syntax +++')
+            loader = self.load_module(mapping)
+            self.assertRaises(SyntaxError, loader.load_module, self.name)
+            for attr in attributes:
+                self.assertEqual(getattr(orig_module, attr), value)
+
+
+
+def test_main():
+    from test.support import run_unittest
+    run_unittest(ReloadTests)
+
+
+if __name__ == '__main__':
+    test_main()
diff --git a/Lib/importlib/test/source/test_source_encoding.py b/Lib/importlib/test/source/test_source_encoding.py
new file mode 100644 (file)
index 0000000..97096df
--- /dev/null
@@ -0,0 +1,122 @@
+import importlib
+from .. import support
+
+import codecs
+import re
+import sys
+# Because sys.path gets essentially blanked, need to have unicodedata already
+# imported for the parser to use.
+import unicodedata
+import unittest
+
+
+CODING_RE = re.compile(r'coding[:=]\s*([-\w.]+)')
+
+
+class EncodingTest(unittest.TestCase):
+
+    """PEP 3120 makes UTF-8 the default encoding for source code
+    [default encoding].
+
+    PEP 263 specifies how that can change on a per-file basis. Either the first
+    or second line can contain the encoding line [encoding first line]
+    encoding second line]. If the file has the BOM marker it is considered UTF-8
+    implicitly [BOM]. If any encoding is specified it must be UTF-8, else it is
+    an error [BOM and utf-8][BOM conflict].
+
+    """
+
+    variable = '\u00fc'
+    character = '\u00c9'
+    source_line = "{0} = '{1}'\n".format(variable, character)
+    module_name = '_temp'
+
+    def run_test(self, source):
+        with support.create_modules(self.module_name) as mapping:
+            with open(mapping[self.module_name], 'wb')as file:
+                file.write(source)
+            loader = importlib._PyFileLoader(self.module_name,
+                                       mapping[self.module_name], False)
+            return loader.load_module(self.module_name)
+
+    def create_source(self, encoding):
+        encoding_line = "# coding={0}".format(encoding)
+        assert CODING_RE.search(encoding_line)
+        source_lines = [encoding_line.encode('utf-8')]
+        source_lines.append(self.source_line.encode(encoding))
+        return b'\n'.join(source_lines)
+
+    def test_non_obvious_encoding(self):
+        # Make sure that an encoding that has never been a standard one for
+        # Python works.
+        encoding_line = "# coding=koi8-r"
+        assert CODING_RE.search(encoding_line)
+        source = "{0}\na=42\n".format(encoding_line).encode("koi8-r")
+        self.run_test(source)
+
+    # [default encoding]
+    def test_default_encoding(self):
+        self.run_test(self.source_line.encode('utf-8'))
+
+    # [encoding first line]
+    def test_encoding_on_first_line(self):
+        encoding = 'Latin-1'
+        source = self.create_source(encoding)
+        self.run_test(source)
+
+    # [encoding second line]
+    def test_encoding_on_second_line(self):
+        source = b"#/usr/bin/python\n" + self.create_source('Latin-1')
+        self.run_test(source)
+
+    # [BOM]
+    def test_bom(self):
+        self.run_test(codecs.BOM_UTF8 + self.source_line.encode('utf-8'))
+
+    # [BOM and utf-8]
+    def test_bom_and_utf_8(self):
+        source = codecs.BOM_UTF8 + self.create_source('utf-8')
+        self.run_test(source)
+
+    # [BOM conflict]
+    def test_bom_conflict(self):
+        source = codecs.BOM_UTF8 + self.create_source('latin-1')
+        self.assertRaises(SyntaxError, self.run_test, source)
+
+
+class LineEndingTest(unittest.TestCase):
+
+    r"""Source written with the three types of line endings (\n, \r\n, \r)
+    need to be readable [cr][crlf][lf]."""
+
+    def run_test(self, line_ending):
+        module_name = '_temp'
+        source_lines = [b"a = 42", b"b = -13", b'']
+        source = line_ending.join(source_lines)
+        with support.create_modules(module_name) as mapping:
+            with open(mapping[module_name], 'wb') as file:
+                file.write(source)
+            loader = importlib._PyFileLoader(module_name, mapping[module_name],
+                                                False)
+            return loader.load_module(module_name)
+
+    # [cr]
+    def test_cr(self):
+        self.run_test(b'\r')
+
+    # [crlf]
+    def test_crlf(self):
+        self.run_test(b'\r\n')
+
+    # [lf]
+    def test_lf(self):
+        self.run_test(b'\n')
+
+
+def test_main():
+    from test.support import run_unittest
+    run_unittest(EncodingTest, LineEndingTest)
+
+
+if __name__ == '__main__':
+    test_main()
diff --git a/Lib/importlib/test/support.py b/Lib/importlib/test/support.py
new file mode 100644 (file)
index 0000000..4e63cd1
--- /dev/null
@@ -0,0 +1,223 @@
+from importlib import Import
+
+from contextlib import contextmanager
+from functools import update_wrapper
+import imp
+import os.path
+from test.support import unlink
+import sys
+from tempfile import gettempdir
+
+
+using___import__ = False
+
+def import_(*args, **kwargs):
+    """Delegate to allow for injecting different implementations of import."""
+    if using___import__:
+        return __import__(*args, **kwargs)
+    return Import()(*args, **kwargs)
+
+def importlib_only(fxn):
+    """Decorator to mark which tests are not supported by the current
+    implementation of __import__()."""
+    def inner(*args, **kwargs):
+        if using___import__:
+            return
+        else:
+            return fxn(*args, **kwargs)
+    update_wrapper(inner, fxn)
+    return inner
+
+def writes_bytecode(fxn):
+    """Decorator that returns the function if writing bytecode is enabled, else
+    a stub function that accepts anything and simply returns None."""
+    if sys.dont_write_bytecode:
+        return lambda *args, **kwargs: None
+    else:
+        return fxn
+
+@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
+
+@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)
+
+
+@contextmanager
+def create_modules(*names):
+    """Temporarily create each named module with an attribute (named 'attr')
+    that contains the name passed into the context manager that caused the
+    creation of the module.
+
+    All files are created in a temporary directory specified by
+    tempfile.gettempdir(). This directory is inserted at the beginning of
+    sys.path. When the context manager exits all created files (source and
+    bytecode) are explicitly deleted.
+
+    No magic is performed when creating packages! This means that if you create
+    a module within a package you must also create the package's __init__ as
+    well.
+
+    """
+    source = 'attr = {0!r}'
+    created_paths = []
+    mapping = {}
+    try:
+        temp_dir = gettempdir()
+        mapping['.root'] = temp_dir
+        import_names = set()
+        for name in names:
+            if not name.endswith('__init__'):
+                import_name = name
+            else:
+                import_name = name[:-len('.__init__')]
+            import_names.add(import_name)
+            if import_name in sys.modules:
+                del sys.modules[import_name]
+            name_parts = name.split('.')
+            file_path = temp_dir
+            for directory in name_parts[:-1]:
+                file_path = os.path.join(file_path, directory)
+                if not os.path.exists(file_path):
+                    os.mkdir(file_path)
+                    created_paths.append(file_path)
+            file_path = os.path.join(file_path, name_parts[-1] + '.py')
+            with open(file_path, 'w') as file:
+                file.write(source.format(name))
+            created_paths.append(file_path)
+            mapping[name] = file_path
+        uncache_manager = uncache(*import_names)
+        uncache_manager.__enter__()
+        state_manager = import_state(path=[temp_dir])
+        state_manager.__enter__()
+        yield mapping
+    finally:
+        state_manager.__exit__(None, None, None)
+        uncache_manager.__exit__(None, None, None)
+        # Reverse the order for path removal to unroll directory creation.
+        for path in reversed(created_paths):
+            if file_path.endswith('.py'):
+                unlink(path)
+                unlink(path + 'c')
+                unlink(path + 'o')
+            else:
+                os.rmdir(path)
+
+
+class mock_modules:
+
+    """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__ = '<mock __file__>'
+            module.__package__ = package
+            module.attr = name
+            if import_name != name:
+                module.__path__ = ['<mock __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)
+
+
+def mock_path_hook(*entries, importer):
+    """A mock sys.path_hooks entry."""
+    def hook(entry):
+        if entry not in entries:
+            raise ImportError
+        return importer
+    return hook
+
+
+def bytecode_path(source_path):
+    for suffix, _, type_ in imp.get_suffixes():
+        if type_ == imp.PY_COMPILED:
+            bc_suffix = suffix
+            break
+    else:
+        raise ValueError("no bytecode suffix is defined")
+    return os.path.splitext(source_path)[0] + bc_suffix
diff --git a/Lib/importlib/test/test_api.py b/Lib/importlib/test/test_api.py
new file mode 100644 (file)
index 0000000..75053a0
--- /dev/null
@@ -0,0 +1,62 @@
+import unittest
+import importlib
+from . import support
+
+
+class ImportModuleTests(unittest.TestCase):
+
+    """Test importlib.import_module."""
+
+    def test_module_import(self):
+        # Test importing a top-level module.
+        with support.mock_modules('top_level') as mock:
+            with support.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 support.mock_modules(pkg_long_name, name) as mock:
+            with support.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 support.mock_modules(pkg_long_name, absolute_name) as mock:
+            with support.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 support.mock_modules(pkg_long_name, name) as mock:
+            with support.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.support import run_unittest
+    run_unittest(ImportModuleTests)
+
+
+if __name__ == '__main__':
+    test_main()
diff --git a/Lib/test/test_importlib.py b/Lib/test/test_importlib.py
new file mode 100644 (file)
index 0000000..cd13e32
--- /dev/null
@@ -0,0 +1,10 @@
+from test.support import run_unittest
+import importlib.test
+
+
+def test_main():
+    run_unittest(importlib.test.test_suite('importlib.test'))
+
+
+if __name__ == '__main__':
+    test_main()