diff --git a/Lib/importlib/NOTES b/Lib/importlib/NOTES new file mode 100644 index 00000000000..134f112eabf --- /dev/null +++ b/Lib/importlib/NOTES @@ -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 index 00000000000..b59c9c4656c --- /dev/null +++ b/Lib/importlib/__init__.py @@ -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 index 00000000000..68f00178990 --- /dev/null +++ b/Lib/importlib/_bootstrap.py @@ -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 index 00000000000..64927073354 --- /dev/null +++ b/Lib/importlib/test/__init__.py @@ -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 index 00000000000..31a3b5f7d46 --- /dev/null +++ b/Lib/importlib/test/builtin/__init__.py @@ -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 index 00000000000..5262aaab921 --- /dev/null +++ b/Lib/importlib/test/builtin/test_finder.py @@ -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 index 00000000000..5aa3d794766 --- /dev/null +++ b/Lib/importlib/test/builtin/test_loader.py @@ -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 index 00000000000..2ec584072d0 --- /dev/null +++ b/Lib/importlib/test/extension/__init__.py @@ -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 index 00000000000..c566b238937 --- /dev/null +++ b/Lib/importlib/test/extension/test_case_sensitivity.py @@ -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 index 00000000000..5766910a1b4 --- /dev/null +++ b/Lib/importlib/test/extension/test_finder.py @@ -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 index 00000000000..4f2a0d87e2c --- /dev/null +++ b/Lib/importlib/test/extension/test_loader.py @@ -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 index 00000000000..f1774fda3de --- /dev/null +++ b/Lib/importlib/test/extension/test_path_hook.py @@ -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 index 00000000000..9bbf85a58e2 --- /dev/null +++ b/Lib/importlib/test/finder_tests.py @@ -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 index 00000000000..2945eeb0bc1 --- /dev/null +++ b/Lib/importlib/test/frozen/__init__.py @@ -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 index 00000000000..e08b89e33b3 --- /dev/null +++ b/Lib/importlib/test/frozen/support.py @@ -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 index 00000000000..254101978ec --- /dev/null +++ b/Lib/importlib/test/frozen/test_finder.py @@ -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('') + 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 index 00000000000..b37ae7f0232 --- /dev/null +++ b/Lib/importlib/test/frozen/test_loader.py @@ -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__': '', + '__package__': None, '__path__': ['__phello__']} + + +class SubmoduleLoaderTests(LoaderTests): + + name = '__phello__.spam' + verification = {'__name__': '__phello__.spam', '__file__': '', + '__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 index 00000000000..fdf7661dc0b --- /dev/null +++ b/Lib/importlib/test/import_/__init__.py @@ -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 index 00000000000..64dab3ad6eb --- /dev/null +++ b/Lib/importlib/test/import_/test___package__.py @@ -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__': ''} + 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 index 00000000000..57690d432b3 --- /dev/null +++ b/Lib/importlib/test/import_/test_caching.py @@ -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 index 00000000000..884b516d36d --- /dev/null +++ b/Lib/importlib/test/import_/test_fromlist.py @@ -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 index 00000000000..37f40d7e4ec --- /dev/null +++ b/Lib/importlib/test/import_/test_meta_path.py @@ -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 index 00000000000..013bbdc2df7 --- /dev/null +++ b/Lib/importlib/test/import_/test_packages.py @@ -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 index 00000000000..c939907af18 --- /dev/null +++ b/Lib/importlib/test/import_/test_path.py @@ -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 index 00000000000..73ef530d4f7 --- /dev/null +++ b/Lib/importlib/test/import_/test_relative_imports.py @@ -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 index 00000000000..8d7c49dc9c5 --- /dev/null +++ b/Lib/importlib/test/source/__init__.py @@ -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 index 00000000000..1a5ff2f206c --- /dev/null +++ b/Lib/importlib/test/source/test_case_sensitivity.py @@ -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 index 00000000000..cf80799399a --- /dev/null +++ b/Lib/importlib/test/source/test_finder.py @@ -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 index 00000000000..249bdc0c7c1 --- /dev/null +++ b/Lib/importlib/test/source/test_loader.py @@ -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 index 00000000000..ef410779c01 --- /dev/null +++ b/Lib/importlib/test/source/test_path_hook.py @@ -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 index 00000000000..e123a2803f8 --- /dev/null +++ b/Lib/importlib/test/source/test_reload.py @@ -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 = '' + 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 index 00000000000..97096df3981 --- /dev/null +++ b/Lib/importlib/test/source/test_source_encoding.py @@ -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 index 00000000000..4e63cd1de86 --- /dev/null +++ b/Lib/importlib/test/support.py @@ -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__ = '' + module.__package__ = package + module.attr = name + if import_name != name: + module.__path__ = [''] + self.modules[import_name] = module + + def __getitem__(self, name): + return self.modules[name] + + def find_module(self, fullname, path=None): + if fullname not in self.modules: + return None + else: + return self + + def load_module(self, fullname): + if fullname not in self.modules: + raise ImportError + else: + sys.modules[fullname] = self.modules[fullname] + return self.modules[fullname] + + def __enter__(self): + self._uncache = uncache(*self.modules.keys()) + self._uncache.__enter__() + return self + + def __exit__(self, *exc_info): + self._uncache.__exit__(None, None, None) + + +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 index 00000000000..75053a074c9 --- /dev/null +++ b/Lib/importlib/test/test_api.py @@ -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 index 00000000000..cd13e329edd --- /dev/null +++ b/Lib/test/test_importlib.py @@ -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()