Skip to content

Commit 9d7b2c0

Browse files
brandtbucherncoghlan
authored andcommitted
bpo-35936: Updates to modulefinder (GH-11787)
* Properly handle SyntaxErrors in Python source files. SyntaxErrors in the target module will rise normally, while SyntaxErrors in dependencies will be added to badmodules. This includes a new regression test. * Fix name collision bug. This fixes an issue where a "fromlist" import with the same name as a previously failed import would be incorrectly added to badmodules. This includes a new regression test. * Replace mutable default values. Bound empty lists have been replaced with the "if param is None" idiom. * Replace deprecated imp usage. Constants imported from imp have been moved to private module-level constants, and ModuleFinder.find_module has been refactored to use importlib. Other than an improvement on how frozen builtin imports are reported (as the frozen imports they are, rather than the stdlib modules they *may* have originated from), these changes maintain complete compatibility with past versions... including odd behavior for returning relative (below current directory, but not a C extension) vs. absolute (above current directory, or a C extension) paths. Patch by Brandt Bucher.
1 parent 2dad960 commit 9d7b2c0

File tree

6 files changed

+116
-16
lines changed

6 files changed

+116
-16
lines changed

Lib/modulefinder.py

Lines changed: 76 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@
88
import sys
99
import types
1010
import warnings
11-
with warnings.catch_warnings():
12-
warnings.simplefilter('ignore', DeprecationWarning)
13-
import imp
11+
1412

1513
LOAD_CONST = dis.opmap['LOAD_CONST']
1614
IMPORT_NAME = dis.opmap['IMPORT_NAME']
@@ -19,6 +17,16 @@
1917
STORE_OPS = STORE_NAME, STORE_GLOBAL
2018
EXTENDED_ARG = dis.EXTENDED_ARG
2119

20+
# Old imp constants:
21+
22+
_SEARCH_ERROR = 0
23+
_PY_SOURCE = 1
24+
_PY_COMPILED = 2
25+
_C_EXTENSION = 3
26+
_PKG_DIRECTORY = 5
27+
_C_BUILTIN = 6
28+
_PY_FROZEN = 7
29+
2230
# Modulefinder does a good job at simulating Python's, but it can not
2331
# handle __path__ modifications packages make at runtime. Therefore there
2432
# is a mechanism whereby you can register extra paths in this map for a
@@ -43,6 +51,54 @@ def ReplacePackage(oldname, newname):
4351
replacePackageMap[oldname] = newname
4452

4553

54+
def _find_module(name, path=None):
55+
"""An importlib reimplementation of imp.find_module (for our purposes)."""
56+
57+
# It's necessary to clear the caches for our Finder first, in case any
58+
# modules are being added/deleted/modified at runtime. In particular,
59+
# test_modulefinder.py changes file tree contents in a cache-breaking way:
60+
61+
importlib.machinery.PathFinder.invalidate_caches()
62+
63+
spec = importlib.machinery.PathFinder.find_spec(name, path)
64+
65+
if spec is None:
66+
raise ImportError("No module named {name!r}".format(name=name), name=name)
67+
68+
# Some special cases:
69+
70+
if spec.loader is importlib.machinery.BuiltinImporter:
71+
return None, None, ("", "", _C_BUILTIN)
72+
73+
if spec.loader is importlib.machinery.FrozenImporter:
74+
return None, None, ("", "", _PY_FROZEN)
75+
76+
file_path = spec.origin
77+
78+
if spec.loader.is_package(name):
79+
return None, os.path.dirname(file_path), ("", "", _PKG_DIRECTORY)
80+
81+
if isinstance(spec.loader, importlib.machinery.SourceFileLoader):
82+
kind = _PY_SOURCE
83+
mode = "r"
84+
85+
elif isinstance(spec.loader, importlib.machinery.ExtensionFileLoader):
86+
kind = _C_EXTENSION
87+
mode = "rb"
88+
89+
elif isinstance(spec.loader, importlib.machinery.SourcelessFileLoader):
90+
kind = _PY_COMPILED
91+
mode = "rb"
92+
93+
else: # Should never happen.
94+
return None, None, ("", "", _SEARCH_ERROR)
95+
96+
file = open(file_path, mode)
97+
suffix = os.path.splitext(file_path)[-1]
98+
99+
return file, file_path, (suffix, mode, kind)
100+
101+
46102
class Module:
47103

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

70126
class ModuleFinder:
71127

72-
def __init__(self, path=None, debug=0, excludes=[], replace_paths=[]):
128+
def __init__(self, path=None, debug=0, excludes=None, replace_paths=None):
73129
if path is None:
74130
path = sys.path
75131
self.path = path
76132
self.modules = {}
77133
self.badmodules = {}
78134
self.debug = debug
79135
self.indent = 0
80-
self.excludes = excludes
81-
self.replace_paths = replace_paths
136+
self.excludes = excludes if excludes is not None else []
137+
self.replace_paths = replace_paths if replace_paths is not None else []
82138
self.processed_paths = [] # Used in debugging only
83139

84140
def msg(self, level, str, *args):
@@ -105,14 +161,14 @@ def msgout(self, *args):
105161
def run_script(self, pathname):
106162
self.msg(2, "run_script", pathname)
107163
with open(pathname) as fp:
108-
stuff = ("", "r", imp.PY_SOURCE)
164+
stuff = ("", "r", _PY_SOURCE)
109165
self.load_module('__main__', fp, pathname, stuff)
110166

111167
def load_file(self, pathname):
112168
dir, name = os.path.split(pathname)
113169
name, ext = os.path.splitext(name)
114170
with open(pathname) as fp:
115-
stuff = (ext, "r", imp.PY_SOURCE)
171+
stuff = (ext, "r", _PY_SOURCE)
116172
self.load_module(name, fp, pathname, stuff)
117173

118174
def import_hook(self, name, caller=None, fromlist=None, level=-1):
@@ -279,13 +335,13 @@ def import_module(self, partname, fqname, parent):
279335
def load_module(self, fqname, fp, pathname, file_info):
280336
suffix, mode, type = file_info
281337
self.msgin(2, "load_module", fqname, fp and "fp", pathname)
282-
if type == imp.PKG_DIRECTORY:
338+
if type == _PKG_DIRECTORY:
283339
m = self.load_package(fqname, pathname)
284340
self.msgout(2, "load_module ->", m)
285341
return m
286-
if type == imp.PY_SOURCE:
342+
if type == _PY_SOURCE:
287343
co = compile(fp.read()+'\n', pathname, 'exec')
288-
elif type == imp.PY_COMPILED:
344+
elif type == _PY_COMPILED:
289345
try:
290346
data = fp.read()
291347
importlib._bootstrap_external._classify_pyc(data, fqname, {})
@@ -323,17 +379,20 @@ def _safe_import_hook(self, name, caller, fromlist, level=-1):
323379
except ImportError as msg:
324380
self.msg(2, "ImportError:", str(msg))
325381
self._add_badmodule(name, caller)
382+
except SyntaxError as msg:
383+
self.msg(2, "SyntaxError:", str(msg))
384+
self._add_badmodule(name, caller)
326385
else:
327386
if fromlist:
328387
for sub in fromlist:
329-
if sub in self.badmodules:
330-
self._add_badmodule(sub, caller)
388+
fullname = name + "." + sub
389+
if fullname in self.badmodules:
390+
self._add_badmodule(fullname, caller)
331391
continue
332392
try:
333393
self.import_hook(name, caller, [sub], level=level)
334394
except ImportError as msg:
335395
self.msg(2, "ImportError:", str(msg))
336-
fullname = name + "." + sub
337396
self._add_badmodule(fullname, caller)
338397

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

446505
if path is None:
447506
if name in sys.builtin_module_names:
448-
return (None, None, ("", "", imp.C_BUILTIN))
507+
return (None, None, ("", "", _C_BUILTIN))
449508

450509
path = self.path
451-
return imp.find_module(name, path)
510+
511+
return _find_module(name, path)
452512

453513
def report(self):
454514
"""Print a report to stdout, listing the found modules with their

Lib/test/test_modulefinder.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,33 @@ def foo(): pass
218218
""
219219
]
220220

221+
syntax_error_test = [
222+
"a.module",
223+
["a", "a.module", "b"],
224+
["b.module"], [],
225+
"""\
226+
a/__init__.py
227+
a/module.py
228+
import b.module
229+
b/__init__.py
230+
b/module.py
231+
? # SyntaxError: invalid syntax
232+
"""]
233+
234+
235+
same_name_as_bad_test = [
236+
"a.module",
237+
["a", "a.module", "b", "b.c"],
238+
["c"], [],
239+
"""\
240+
a/__init__.py
241+
a/module.py
242+
import c
243+
from b import c
244+
b/__init__.py
245+
b/c.py
246+
"""]
247+
221248

222249
def open_file(path):
223250
dirname = os.path.dirname(path)
@@ -299,6 +326,12 @@ def test_relative_imports_3(self):
299326
def test_relative_imports_4(self):
300327
self._do_test(relative_import_test_4)
301328

329+
def test_syntax_error(self):
330+
self._do_test(syntax_error_test)
331+
332+
def test_same_name_as_bad(self):
333+
self._do_test(same_name_as_bad_test)
334+
302335
def test_bytecode(self):
303336
base_path = os.path.join(TEST_DIR, 'a')
304337
source_path = base_path + importlib.machinery.SOURCE_SUFFIXES[0]

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ Ian Bruntlett
222222
Floris Bruynooghe
223223
Matt Bryant
224224
Stan Bubrouski
225+
Brandt Bucher
225226
Colm Buckley
226227
Erik de Bueger
227228
Jan-Hein Bührman
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:mod:`modulefinder` no longer crashes when encountering syntax errors in followed imports.
2+
Patch by Brandt Bucher.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:mod:`modulefinder` correctly handles modules that have the same name as a bad package.
2+
Patch by Brandt Bucher.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:mod:`modulefinder` no longer depends on the deprecated :mod:`imp` module, and the initializer for :class:`modulefinder.ModuleFinder` now has immutable default arguments.
2+
Patch by Brandt Bucher.

0 commit comments

Comments
 (0)