Skip to content

bpo-35936: Updates to modulefinder #11787

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 43 commits into from
Apr 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
7c7dd45
Properly handle SyntaxErrors in Python source files.
brandtbucher Feb 7, 2019
8f98ca6
Fix name collision bug.
brandtbucher Feb 7, 2019
c00c4b6
Replace mutable default values.
brandtbucher Feb 7, 2019
94e38d4
Replace deprecated imp usage.
brandtbucher Feb 7, 2019
9bda94b
Fixed whitespace.
brandtbucher Feb 8, 2019
c366485
Handle differing drives on Windows.
brandtbucher Feb 8, 2019
dc9180d
Again, fixed whitespace.
brandtbucher Feb 8, 2019
faf2902
📜🤖 Added by blurb_it.
blurb-it[bot] Feb 8, 2019
a1db2ec
Fix relative imports.
brandtbucher Feb 8, 2019
bd73fa5
Fix import side-effects.
brandtbucher Feb 9, 2019
210edad
Wait for sys.path to update.
brandtbucher Feb 9, 2019
3a9dd35
Bump sleep time.
brandtbucher Feb 9, 2019
2763440
WIP: Replace sleep with busy wait.
brandtbucher Feb 9, 2019
f9939bf
Remove busy wait.
brandtbucher Feb 9, 2019
ddb8752
Remove NEWS entry.
brandtbucher Feb 9, 2019
8d7f272
Mutate sys.path in-place.
brandtbucher Feb 9, 2019
1a594f0
Enter RLock during import search.
brandtbucher Feb 9, 2019
6dabded
Try multiprocessing.RLock instead of threading.
brandtbucher Feb 9, 2019
7095d14
Try a couple of possible fixes for failing lookups.
brandtbucher Feb 9, 2019
1aea4af
Remove sys._clear_type_cache calls.
brandtbucher Feb 9, 2019
603481a
📜🤖 Added by blurb_it.
blurb-it[bot] Feb 10, 2019
f022f83
📜🤖 Added by blurb_it.
blurb-it[bot] Feb 10, 2019
2568d5a
📜🤖 Added by blurb_it.
blurb-it[bot] Feb 10, 2019
bbae72e
Break out new code into standalone function.
brandtbucher Feb 10, 2019
6833792
Don't modify import state in-place.
brandtbucher Feb 10, 2019
0af9f08
Reorder new import.
brandtbucher Feb 10, 2019
4bb452e
Minor style stuff.
brandtbucher Feb 11, 2019
a9fd3c6
Replace importlib.util.find_spec with importlib.machinery.PathFinder.…
brandtbucher Feb 13, 2019
826eef5
Invalidate caches before find_spec call.
brandtbucher Feb 13, 2019
de22d67
Fix whitespace.
brandtbucher Feb 13, 2019
4de4b3d
Remove duplicate-bpo NEWS entries.
brandtbucher Feb 13, 2019
3085a62
📜🤖 Added by blurb_it.
blurb-it[bot] Feb 13, 2019
7b76e58
📜🤖 Added by blurb_it.
blurb-it[bot] Feb 13, 2019
744de0c
Empty commit.
brandtbucher Feb 13, 2019
7371742
Remove NEWS entry.
brandtbucher Feb 16, 2019
540e819
📜🤖 Added by blurb_it.
blurb-it[bot] Feb 16, 2019
d6f19a6
Remove loader subclass checks.
brandtbucher Feb 17, 2019
04fc93a
Use spec.loader for package checking.
brandtbucher Feb 17, 2019
a506dc4
Remove sys.path from path.
brandtbucher Feb 17, 2019
d1fa6af
Use loader type to classify imports.
brandtbucher Feb 17, 2019
8840bc0
Remove absolute/relative path formatting.
brandtbucher Feb 17, 2019
654461b
Reorder branching.
brandtbucher Feb 17, 2019
9a732c1
Add comments on cache invalidation.
brandtbucher Feb 17, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 76 additions & 16 deletions Lib/modulefinder.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@
import sys
import types
import warnings
with warnings.catch_warnings():
warnings.simplefilter('ignore', DeprecationWarning)
import imp


LOAD_CONST = dis.opmap['LOAD_CONST']
IMPORT_NAME = dis.opmap['IMPORT_NAME']
Expand All @@ -19,6 +17,16 @@
STORE_OPS = STORE_NAME, STORE_GLOBAL
EXTENDED_ARG = dis.EXTENDED_ARG

# Old imp constants:

_SEARCH_ERROR = 0
_PY_SOURCE = 1
_PY_COMPILED = 2
_C_EXTENSION = 3
_PKG_DIRECTORY = 5
_C_BUILTIN = 6
_PY_FROZEN = 7

# Modulefinder does a good job at simulating Python's, but it can not
# handle __path__ modifications packages make at runtime. Therefore there
# is a mechanism whereby you can register extra paths in this map for a
Expand All @@ -43,6 +51,54 @@ def ReplacePackage(oldname, newname):
replacePackageMap[oldname] = newname


def _find_module(name, path=None):
"""An importlib reimplementation of imp.find_module (for our purposes)."""

# It's necessary to clear the caches for our Finder first, in case any
# modules are being added/deleted/modified at runtime. In particular,
# test_modulefinder.py changes file tree contents in a cache-breaking way:

importlib.machinery.PathFinder.invalidate_caches()

spec = importlib.machinery.PathFinder.find_spec(name, path)

if spec is None:
raise ImportError("No module named {name!r}".format(name=name), name=name)

# Some special cases:

if spec.loader is importlib.machinery.BuiltinImporter:
return None, None, ("", "", _C_BUILTIN)

if spec.loader is importlib.machinery.FrozenImporter:
return None, None, ("", "", _PY_FROZEN)

file_path = spec.origin

if spec.loader.is_package(name):
return None, os.path.dirname(file_path), ("", "", _PKG_DIRECTORY)

if isinstance(spec.loader, importlib.machinery.SourceFileLoader):
kind = _PY_SOURCE
mode = "r"

elif isinstance(spec.loader, importlib.machinery.ExtensionFileLoader):
kind = _C_EXTENSION
mode = "rb"

elif isinstance(spec.loader, importlib.machinery.SourcelessFileLoader):
kind = _PY_COMPILED
mode = "rb"

else: # Should never happen.
return None, None, ("", "", _SEARCH_ERROR)

file = open(file_path, mode)
suffix = os.path.splitext(file_path)[-1]

return file, file_path, (suffix, mode, kind)


class Module:

def __init__(self, name, file=None, path=None):
Expand All @@ -69,16 +125,16 @@ def __repr__(self):

class ModuleFinder:

def __init__(self, path=None, debug=0, excludes=[], replace_paths=[]):
def __init__(self, path=None, debug=0, excludes=None, replace_paths=None):
if path is None:
path = sys.path
self.path = path
self.modules = {}
self.badmodules = {}
self.debug = debug
self.indent = 0
self.excludes = excludes
self.replace_paths = replace_paths
self.excludes = excludes if excludes is not None else []
self.replace_paths = replace_paths if replace_paths is not None else []
self.processed_paths = [] # Used in debugging only

def msg(self, level, str, *args):
Expand All @@ -105,14 +161,14 @@ def msgout(self, *args):
def run_script(self, pathname):
self.msg(2, "run_script", pathname)
with open(pathname) as fp:
stuff = ("", "r", imp.PY_SOURCE)
stuff = ("", "r", _PY_SOURCE)
self.load_module('__main__', fp, pathname, stuff)

def load_file(self, pathname):
dir, name = os.path.split(pathname)
name, ext = os.path.splitext(name)
with open(pathname) as fp:
stuff = (ext, "r", imp.PY_SOURCE)
stuff = (ext, "r", _PY_SOURCE)
self.load_module(name, fp, pathname, stuff)

def import_hook(self, name, caller=None, fromlist=None, level=-1):
Expand Down Expand Up @@ -279,13 +335,13 @@ def import_module(self, partname, fqname, parent):
def load_module(self, fqname, fp, pathname, file_info):
suffix, mode, type = file_info
self.msgin(2, "load_module", fqname, fp and "fp", pathname)
if type == imp.PKG_DIRECTORY:
if type == _PKG_DIRECTORY:
m = self.load_package(fqname, pathname)
self.msgout(2, "load_module ->", m)
return m
if type == imp.PY_SOURCE:
if type == _PY_SOURCE:
co = compile(fp.read()+'\n', pathname, 'exec')
elif type == imp.PY_COMPILED:
elif type == _PY_COMPILED:
try:
data = fp.read()
importlib._bootstrap_external._classify_pyc(data, fqname, {})
Expand Down Expand Up @@ -323,17 +379,20 @@ def _safe_import_hook(self, name, caller, fromlist, level=-1):
except ImportError as msg:
self.msg(2, "ImportError:", str(msg))
self._add_badmodule(name, caller)
except SyntaxError as msg:
self.msg(2, "SyntaxError:", str(msg))
self._add_badmodule(name, caller)
else:
if fromlist:
for sub in fromlist:
if sub in self.badmodules:
self._add_badmodule(sub, caller)
fullname = name + "." + sub
if fullname in self.badmodules:
self._add_badmodule(fullname, caller)
continue
try:
self.import_hook(name, caller, [sub], level=level)
except ImportError as msg:
self.msg(2, "ImportError:", str(msg))
fullname = name + "." + sub
self._add_badmodule(fullname, caller)

def scan_opcodes(self, co):
Expand Down Expand Up @@ -445,10 +504,11 @@ def find_module(self, name, path, parent=None):

if path is None:
if name in sys.builtin_module_names:
return (None, None, ("", "", imp.C_BUILTIN))
return (None, None, ("", "", _C_BUILTIN))

path = self.path
return imp.find_module(name, path)

return _find_module(name, path)

def report(self):
"""Print a report to stdout, listing the found modules with their
Expand Down
33 changes: 33 additions & 0 deletions Lib/test/test_modulefinder.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,33 @@ def foo(): pass
""
]

syntax_error_test = [
"a.module",
["a", "a.module", "b"],
["b.module"], [],
"""\
a/__init__.py
a/module.py
import b.module
b/__init__.py
b/module.py
? # SyntaxError: invalid syntax
"""]


same_name_as_bad_test = [
"a.module",
["a", "a.module", "b", "b.c"],
["c"], [],
"""\
a/__init__.py
a/module.py
import c
from b import c
b/__init__.py
b/c.py
"""]


def open_file(path):
dirname = os.path.dirname(path)
Expand Down Expand Up @@ -299,6 +326,12 @@ def test_relative_imports_3(self):
def test_relative_imports_4(self):
self._do_test(relative_import_test_4)

def test_syntax_error(self):
self._do_test(syntax_error_test)

def test_same_name_as_bad(self):
self._do_test(same_name_as_bad_test)

def test_bytecode(self):
base_path = os.path.join(TEST_DIR, 'a')
source_path = base_path + importlib.machinery.SOURCE_SUFFIXES[0]
Expand Down
1 change: 1 addition & 0 deletions Misc/ACKS
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ Ian Bruntlett
Floris Bruynooghe
Matt Bryant
Stan Bubrouski
Brandt Bucher
Colm Buckley
Erik de Bueger
Jan-Hein Bührman
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
:mod:`modulefinder` no longer crashes when encountering syntax errors in followed imports.
Patch by Brandt Bucher.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
:mod:`modulefinder` correctly handles modules that have the same name as a bad package.
Patch by Brandt Bucher.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
:mod:`modulefinder` no longer depends on the deprecated :mod:`imp` module, and the initializer for :class:`modulefinder.ModuleFinder` now has immutable default arguments.
Patch by Brandt Bucher.