diff --git a/Lib/importlib/_bootstrap.py b/Lib/importlib/_bootstrap.py --- a/Lib/importlib/_bootstrap.py +++ b/Lib/importlib/_bootstrap.py @@ -21,6 +21,13 @@ work. One should use importlib as the pu CASE_INSENSITIVE_PLATFORMS = 'win', 'cygwin', 'darwin' + +def _relax_case(): + """True if filenames must be checked case-insensitively.""" + return (any(map(sys.platform.startswith, CASE_INSENSITIVE_PLATFORMS)) and + b'PYTHONCASEOK' in _os.environ) + + def _case_insensitive_ok(directory, check): """Check if the directory contains something matching 'check' exists in the directory. @@ -86,6 +93,10 @@ def _path_join(*args): for x in args if x) +def _path_prepender(*args): + return _path_join(*args) + path_sep + + def _path_exists(path): """Replacement for os.path.exists.""" try: @@ -760,28 +771,85 @@ class _FileFinder: for suffix in detail.suffixes) self.packages = packages self.modules = modules - self.path = path + # Base (directory) path + self.path = path or '.' + self._path_mtime = -1 + self._modules_cache = {} + self._packages_cache = {} def find_module(self, fullname): """Try to find a loader for the specified module.""" tail_module = fullname.rpartition('.')[2] - base_path = _path_join(self.path, tail_module) - if _path_isdir(base_path) and _case_ok(self.path, tail_module): - for suffix, loader in self.packages: + if _relax_case: + tail_module = tail_module.lower() + try: + mtime = _os.stat(self.path).st_mtime + except OSError: + mtime = -1 + if mtime != self._path_mtime: + self._fill_cache() + self._path_mtime = mtime + if tail_module in self._packages_cache: + fn, loader = self._packages_cache[tail_module] + if loader is None: + msg = "Not importing directory {}: missing __init__" + _warnings.warn(msg.format(fn), ImportWarning) + elif tail_module in self._modules_cache: + fn, loader = self._modules_cache[tail_module] + else: + return None + return loader(fullname, _path_join(self.path, fn)) + + def _fill_cache(self): + """Fill the cache of potential modules and packages for this directory.""" + modules_cache = self._modules_cache = {} + packages_cache = self._packages_cache = {} + modules = self.modules + packages = self.packages + modules_suffixes = tuple(suffix for suffix, loader in modules) + path = self.path + l = _os.listdir(path) + path_prefix = _path_prepender(path) + for fn in l: + # Is it a module? + if fn.endswith(modules_suffixes): + # Found a potential module + full_path = path_prefix + fn + if _path_isfile(full_path): + for suffix, loader in modules: + if fn.endswith(suffix): + modname = fn[:-len(suffix)] + if _relax_case: + modname = modname.lower() + modules_cache[modname] = (fn, loader) + break + else: + # Can't get here + assert 0 + continue + # Not a module + # Skip all files that cannot be packages + if not fn.isidentifier(): + continue + base_path = path_prefix + fn + if not _path_isdir(base_path): + continue + # Found a potential package + pkgname = fn + if _relax_case: + pkgname = pkgname.lower() + for suffix, loader in packages: init_filename = '__init__' + suffix full_path = _path_join(base_path, init_filename) if (_path_isfile(full_path) and _case_ok(base_path, init_filename)): - return loader(fullname, full_path) + packages_cache[pkgname] = (_path_join(fn, init_filename), + loader) + break else: - msg = "Not importing directory {}: missing __init__" - _warnings.warn(msg.format(base_path), ImportWarning) - for suffix, loader in self.modules: - mod_filename = tail_module + suffix - full_path = _path_join(self.path, mod_filename) - if _path_isfile(full_path) and _case_ok(self.path, mod_filename): - return loader(fullname, full_path) - return None + # For warnings + packages_cache[pkgname] = (fn, None) + class _SourceFinderDetails: @@ -1093,6 +1161,8 @@ def _setup(sys_module, imp_module): raise ImportError('importlib requires posix or nt') setattr(self_module, '_os', os_module) setattr(self_module, 'path_sep', path_sep) + # Constants + setattr(self_module, '_relax_case', _relax_case()) if sys_module.platform in CASE_INSENSITIVE_PLATFORMS: _case_ok = _case_insensitive_ok