Thomas Heller fixed modulefinder and added a test. Thanks!
This commit is contained in:
parent
2def557aba
commit
fc2a0a8e3c
|
@ -1,13 +1,14 @@
|
||||||
"""Find modules used by a script, using introspection."""
|
"""Find modules used by a script, using introspection."""
|
||||||
|
|
||||||
# This module should be kept compatible with Python 2.2, see PEP 291.
|
# This module should be kept compatible with Python 2.2, see PEP 291.
|
||||||
|
|
||||||
|
from __future__ import generators
|
||||||
import dis
|
import dis
|
||||||
import imp
|
import imp
|
||||||
import marshal
|
import marshal
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import new
|
import new
|
||||||
|
import struct
|
||||||
|
|
||||||
if hasattr(sys.__stdout__, "newlines"):
|
if hasattr(sys.__stdout__, "newlines"):
|
||||||
READ_MODE = "U" # universal line endings
|
READ_MODE = "U" # universal line endings
|
||||||
|
@ -15,11 +16,12 @@ else:
|
||||||
# remain compatible with Python < 2.3
|
# remain compatible with Python < 2.3
|
||||||
READ_MODE = "r"
|
READ_MODE = "r"
|
||||||
|
|
||||||
LOAD_CONST = dis.opname.index('LOAD_CONST')
|
LOAD_CONST = chr(dis.opname.index('LOAD_CONST'))
|
||||||
IMPORT_NAME = dis.opname.index('IMPORT_NAME')
|
IMPORT_NAME = chr(dis.opname.index('IMPORT_NAME'))
|
||||||
STORE_NAME = dis.opname.index('STORE_NAME')
|
STORE_NAME = chr(dis.opname.index('STORE_NAME'))
|
||||||
STORE_GLOBAL = dis.opname.index('STORE_GLOBAL')
|
STORE_GLOBAL = chr(dis.opname.index('STORE_GLOBAL'))
|
||||||
STORE_OPS = [STORE_NAME, STORE_GLOBAL]
|
STORE_OPS = [STORE_NAME, STORE_GLOBAL]
|
||||||
|
HAVE_ARGUMENT = chr(dis.HAVE_ARGUMENT)
|
||||||
|
|
||||||
# Modulefinder does a good job at simulating Python's, but it can not
|
# Modulefinder does a good job at simulating Python's, but it can not
|
||||||
# handle __path__ modifications packages make at runtime. Therefore there
|
# handle __path__ modifications packages make at runtime. Therefore there
|
||||||
|
@ -118,9 +120,9 @@ class ModuleFinder:
|
||||||
stuff = (ext, "r", imp.PY_SOURCE)
|
stuff = (ext, "r", imp.PY_SOURCE)
|
||||||
self.load_module(name, fp, pathname, stuff)
|
self.load_module(name, fp, pathname, stuff)
|
||||||
|
|
||||||
def import_hook(self, name, caller=None, fromlist=None):
|
def import_hook(self, name, caller=None, fromlist=None, level=-1):
|
||||||
self.msg(3, "import_hook", name, caller, fromlist)
|
self.msg(3, "import_hook", name, caller, fromlist, level)
|
||||||
parent = self.determine_parent(caller)
|
parent = self.determine_parent(caller, level=level)
|
||||||
q, tail = self.find_head_package(parent, name)
|
q, tail = self.find_head_package(parent, name)
|
||||||
m = self.load_tail(q, tail)
|
m = self.load_tail(q, tail)
|
||||||
if not fromlist:
|
if not fromlist:
|
||||||
|
@ -129,12 +131,26 @@ class ModuleFinder:
|
||||||
self.ensure_fromlist(m, fromlist)
|
self.ensure_fromlist(m, fromlist)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def determine_parent(self, caller):
|
def determine_parent(self, caller, level=-1):
|
||||||
self.msgin(4, "determine_parent", caller)
|
self.msgin(4, "determine_parent", caller, level)
|
||||||
if not caller:
|
if not caller or level == 0:
|
||||||
self.msgout(4, "determine_parent -> None")
|
self.msgout(4, "determine_parent -> None")
|
||||||
return None
|
return None
|
||||||
pname = caller.__name__
|
pname = caller.__name__
|
||||||
|
if level >= 1: # relative import
|
||||||
|
if caller.__path__:
|
||||||
|
level -= 1
|
||||||
|
if level == 0:
|
||||||
|
parent = self.modules[pname]
|
||||||
|
assert parent is caller
|
||||||
|
self.msgout(4, "determine_parent ->", parent)
|
||||||
|
return parent
|
||||||
|
if pname.count(".") < level:
|
||||||
|
raise ImportError, "relative importpath too deep"
|
||||||
|
pname = ".".join(pname.split(".")[:-level])
|
||||||
|
parent = self.modules[pname]
|
||||||
|
self.msgout(4, "determine_parent ->", parent)
|
||||||
|
return parent
|
||||||
if caller.__path__:
|
if caller.__path__:
|
||||||
parent = self.modules[pname]
|
parent = self.modules[pname]
|
||||||
assert caller is parent
|
assert caller is parent
|
||||||
|
@ -294,13 +310,13 @@ class ModuleFinder:
|
||||||
self.badmodules[name] = {}
|
self.badmodules[name] = {}
|
||||||
self.badmodules[name][caller.__name__] = 1
|
self.badmodules[name][caller.__name__] = 1
|
||||||
|
|
||||||
def _safe_import_hook(self, name, caller, fromlist):
|
def _safe_import_hook(self, name, caller, fromlist, level=-1):
|
||||||
# wrapper for self.import_hook() that won't raise ImportError
|
# wrapper for self.import_hook() that won't raise ImportError
|
||||||
if name in self.badmodules:
|
if name in self.badmodules:
|
||||||
self._add_badmodule(name, caller)
|
self._add_badmodule(name, caller)
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
self.import_hook(name, caller)
|
self.import_hook(name, caller, level=level)
|
||||||
except ImportError, msg:
|
except ImportError, msg:
|
||||||
self.msg(2, "ImportError:", str(msg))
|
self.msg(2, "ImportError:", str(msg))
|
||||||
self._add_badmodule(name, caller)
|
self._add_badmodule(name, caller)
|
||||||
|
@ -311,38 +327,83 @@ class ModuleFinder:
|
||||||
self._add_badmodule(sub, caller)
|
self._add_badmodule(sub, caller)
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
self.import_hook(name, caller, [sub])
|
self.import_hook(name, caller, [sub], level=level)
|
||||||
except ImportError, msg:
|
except ImportError, msg:
|
||||||
self.msg(2, "ImportError:", str(msg))
|
self.msg(2, "ImportError:", str(msg))
|
||||||
fullname = name + "." + sub
|
fullname = name + "." + sub
|
||||||
self._add_badmodule(fullname, caller)
|
self._add_badmodule(fullname, caller)
|
||||||
|
|
||||||
|
def scan_opcodes(self, co,
|
||||||
|
unpack = struct.unpack):
|
||||||
|
# Scan the code, and yield 'interesting' opcode combinations
|
||||||
|
# Version for Python 2.4 and older
|
||||||
|
code = co.co_code
|
||||||
|
names = co.co_names
|
||||||
|
consts = co.co_consts
|
||||||
|
while code:
|
||||||
|
c = code[0]
|
||||||
|
if c in STORE_OPS:
|
||||||
|
oparg, = unpack('<H', code[1:3])
|
||||||
|
yield "store", (names[oparg],)
|
||||||
|
code = code[3:]
|
||||||
|
continue
|
||||||
|
if c == LOAD_CONST and code[3] == IMPORT_NAME:
|
||||||
|
oparg_1, oparg_2 = unpack('<xHxH', code[:6])
|
||||||
|
yield "import", (consts[oparg_1], names[oparg_2])
|
||||||
|
code = code[6:]
|
||||||
|
continue
|
||||||
|
if c >= HAVE_ARGUMENT:
|
||||||
|
code = code[3:]
|
||||||
|
else:
|
||||||
|
code = code[1:]
|
||||||
|
|
||||||
|
def scan_opcodes_25(self, co,
|
||||||
|
unpack = struct.unpack):
|
||||||
|
# Scan the code, and yield 'interesting' opcode combinations
|
||||||
|
# Python 2.5 version (has absolute and relative imports)
|
||||||
|
code = co.co_code
|
||||||
|
names = co.co_names
|
||||||
|
consts = co.co_consts
|
||||||
|
LOAD_LOAD_AND_IMPORT = LOAD_CONST + LOAD_CONST + IMPORT_NAME
|
||||||
|
while code:
|
||||||
|
c = code[0]
|
||||||
|
if c in STORE_OPS:
|
||||||
|
oparg, = unpack('<H', code[1:3])
|
||||||
|
yield "store", (names[oparg],)
|
||||||
|
code = code[3:]
|
||||||
|
continue
|
||||||
|
if code[:9:3] == LOAD_LOAD_AND_IMPORT:
|
||||||
|
oparg_1, oparg_2, oparg_3 = unpack('<xHxHxH', code[:9])
|
||||||
|
level = consts[oparg_1]
|
||||||
|
if level == 0: # absolute import
|
||||||
|
yield "absolute_import", (consts[oparg_2], names[oparg_3])
|
||||||
|
else: # relative import
|
||||||
|
yield "relative_import", (level, consts[oparg_2], names[oparg_3])
|
||||||
|
code = code[9:]
|
||||||
|
continue
|
||||||
|
if c >= HAVE_ARGUMENT:
|
||||||
|
code = code[3:]
|
||||||
|
else:
|
||||||
|
code = code[1:]
|
||||||
|
|
||||||
def scan_code(self, co, m):
|
def scan_code(self, co, m):
|
||||||
code = co.co_code
|
code = co.co_code
|
||||||
n = len(code)
|
if sys.version_info >= (2, 5):
|
||||||
i = 0
|
scanner = self.scan_opcodes_25
|
||||||
fromlist = None
|
else:
|
||||||
while i < n:
|
scanner = self.scan_opcodes
|
||||||
c = code[i]
|
for what, args in scanner(co):
|
||||||
i = i+1
|
if what == "store":
|
||||||
op = ord(c)
|
name, = args
|
||||||
if op >= dis.HAVE_ARGUMENT:
|
m.globalnames[name] = 1
|
||||||
oparg = ord(code[i]) + ord(code[i+1])*256
|
elif what == "absolute_import":
|
||||||
i = i+2
|
fromlist, name = args
|
||||||
if op == LOAD_CONST:
|
|
||||||
# An IMPORT_NAME is always preceded by a LOAD_CONST, it's
|
|
||||||
# a tuple of "from" names, or None for a regular import.
|
|
||||||
# The tuple may contain "*" for "from <mod> import *"
|
|
||||||
fromlist = co.co_consts[oparg]
|
|
||||||
elif op == IMPORT_NAME:
|
|
||||||
assert fromlist is None or type(fromlist) is tuple
|
|
||||||
name = co.co_names[oparg]
|
|
||||||
have_star = 0
|
have_star = 0
|
||||||
if fromlist is not None:
|
if fromlist is not None:
|
||||||
if "*" in fromlist:
|
if "*" in fromlist:
|
||||||
have_star = 1
|
have_star = 1
|
||||||
fromlist = [f for f in fromlist if f != "*"]
|
fromlist = [f for f in fromlist if f != "*"]
|
||||||
self._safe_import_hook(name, m, fromlist)
|
self._safe_import_hook(name, m, fromlist, level=0)
|
||||||
if have_star:
|
if have_star:
|
||||||
# We've encountered an "import *". If it is a Python module,
|
# We've encountered an "import *". If it is a Python module,
|
||||||
# the code has already been parsed and we can suck out the
|
# the code has already been parsed and we can suck out the
|
||||||
|
@ -362,10 +423,17 @@ class ModuleFinder:
|
||||||
m.starimports[name] = 1
|
m.starimports[name] = 1
|
||||||
else:
|
else:
|
||||||
m.starimports[name] = 1
|
m.starimports[name] = 1
|
||||||
elif op in STORE_OPS:
|
elif what == "relative_import":
|
||||||
# keep track of all global names that are assigned to
|
level, fromlist, name = args
|
||||||
name = co.co_names[oparg]
|
if name:
|
||||||
m.globalnames[name] = 1
|
self._safe_import_hook(name, m, fromlist, level=level)
|
||||||
|
else:
|
||||||
|
parent = self.determine_parent(m, level=level)
|
||||||
|
self._safe_import_hook(parent.__name__, None, fromlist, level=0)
|
||||||
|
else:
|
||||||
|
# We don't expect anything else from the generator.
|
||||||
|
raise RuntimeError(what)
|
||||||
|
|
||||||
for c in co.co_consts:
|
for c in co.co_consts:
|
||||||
if isinstance(c, type(co)):
|
if isinstance(c, type(co)):
|
||||||
self.scan_code(c, m)
|
self.scan_code(c, m)
|
||||||
|
|
|
@ -0,0 +1,259 @@
|
||||||
|
import __future__
|
||||||
|
import sys, os
|
||||||
|
import unittest
|
||||||
|
import distutils.dir_util
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from test import test_support
|
||||||
|
|
||||||
|
try: set
|
||||||
|
except NameError: from sets import Set as set
|
||||||
|
|
||||||
|
import modulefinder
|
||||||
|
|
||||||
|
# Note: To test modulefinder with Python 2.2, sets.py and
|
||||||
|
# modulefinder.py must be available - they are not in the standard
|
||||||
|
# library.
|
||||||
|
|
||||||
|
TEST_DIR = tempfile.mkdtemp()
|
||||||
|
TEST_PATH = [TEST_DIR, os.path.dirname(__future__.__file__)]
|
||||||
|
|
||||||
|
# Each test description is a list of 5 items:
|
||||||
|
#
|
||||||
|
# 1. a module name that will be imported by modulefinder
|
||||||
|
# 2. a list of module names that modulefinder is required to find
|
||||||
|
# 3. a list of module names that modulefinder should complain
|
||||||
|
# about because they are not found
|
||||||
|
# 4. a list of module names that modulefinder should complain
|
||||||
|
# about because they MAY be not found
|
||||||
|
# 5. a string specifying packages to create; the format is obvious imo.
|
||||||
|
#
|
||||||
|
# Each package will be created in TEST_DIR, and TEST_DIR will be
|
||||||
|
# removed after the tests again.
|
||||||
|
# Modulefinder searches in a path that contains TEST_DIR, plus
|
||||||
|
# the standard Lib directory.
|
||||||
|
|
||||||
|
maybe_test = [
|
||||||
|
"a.module",
|
||||||
|
["a", "a.module", "sys",
|
||||||
|
"b"],
|
||||||
|
["c"], ["b.something"],
|
||||||
|
"""\
|
||||||
|
a/__init__.py
|
||||||
|
a/module.py
|
||||||
|
from b import something
|
||||||
|
from c import something
|
||||||
|
b/__init__.py
|
||||||
|
from sys import *
|
||||||
|
"""]
|
||||||
|
|
||||||
|
maybe_test_new = [
|
||||||
|
"a.module",
|
||||||
|
["a", "a.module", "sys",
|
||||||
|
"b"],
|
||||||
|
["c"], ["b.something"],
|
||||||
|
"""\
|
||||||
|
a/__init__.py
|
||||||
|
a/module.py
|
||||||
|
from b import something
|
||||||
|
from c import something
|
||||||
|
b/__init__.py
|
||||||
|
from sys import *
|
||||||
|
"""]
|
||||||
|
|
||||||
|
package_test = [
|
||||||
|
"a.module",
|
||||||
|
["a", "a.b", "a.c", "a.module", "mymodule", "sys"],
|
||||||
|
["blahblah", "c"], [],
|
||||||
|
"""\
|
||||||
|
mymodule.py
|
||||||
|
a/__init__.py
|
||||||
|
import blahblah
|
||||||
|
from a import b
|
||||||
|
import c
|
||||||
|
a/module.py
|
||||||
|
import sys
|
||||||
|
from a import b as x
|
||||||
|
from a.c import sillyname
|
||||||
|
a/b.py
|
||||||
|
a/c.py
|
||||||
|
from a.module import x
|
||||||
|
import mymodule as sillyname
|
||||||
|
from sys import version_info
|
||||||
|
"""]
|
||||||
|
|
||||||
|
absolute_import_test = [
|
||||||
|
"a.module",
|
||||||
|
["a", "a.module",
|
||||||
|
"b", "b.x", "b.y", "b.z",
|
||||||
|
"sys", "exceptions"],
|
||||||
|
["blahblah", "z"], [],
|
||||||
|
"""\
|
||||||
|
mymodule.py
|
||||||
|
a/__init__.py
|
||||||
|
a/module.py
|
||||||
|
import sys # sys
|
||||||
|
import blahblah # fails
|
||||||
|
import exceptions # exceptions
|
||||||
|
import b.x # b.x
|
||||||
|
from b import y # b.y
|
||||||
|
from b.z import * # b.z.*
|
||||||
|
a/exceptions.py
|
||||||
|
a/sys.py
|
||||||
|
import mymodule
|
||||||
|
a/b/__init__.py
|
||||||
|
a/b/x.py
|
||||||
|
a/b/y.py
|
||||||
|
a/b/z.py
|
||||||
|
b/__init__.py
|
||||||
|
import z
|
||||||
|
b/unused.py
|
||||||
|
b/x.py
|
||||||
|
b/y.py
|
||||||
|
b/z.py
|
||||||
|
"""]
|
||||||
|
|
||||||
|
relative_import_test = [
|
||||||
|
"a.module",
|
||||||
|
["a", "a.module",
|
||||||
|
"a.b", "a.b.y", "a.b.z",
|
||||||
|
"a.b.c", "a.b.c.moduleC",
|
||||||
|
"a.b.c.d", "a.b.c.e",
|
||||||
|
"a.b.x",
|
||||||
|
"exceptions"],
|
||||||
|
[], [],
|
||||||
|
"""\
|
||||||
|
mymodule.py
|
||||||
|
a/__init__.py
|
||||||
|
from .b import y, z # a.b.y, a.b.z
|
||||||
|
a/module.py
|
||||||
|
import exceptions # exceptions
|
||||||
|
a/exceptions.py
|
||||||
|
a/sys.py
|
||||||
|
a/b/__init__.py
|
||||||
|
from ..b import x # a.b.x
|
||||||
|
#from a.b.c import moduleC
|
||||||
|
from .c import moduleC # a.b.moduleC
|
||||||
|
a/b/x.py
|
||||||
|
a/b/y.py
|
||||||
|
a/b/z.py
|
||||||
|
a/b/g.py
|
||||||
|
a/b/c/__init__.py
|
||||||
|
from ..c import e # a.b.c.e
|
||||||
|
a/b/c/moduleC.py
|
||||||
|
from ..c import d # a.b.c.d
|
||||||
|
a/b/c/d.py
|
||||||
|
a/b/c/e.py
|
||||||
|
a/b/c/x.py
|
||||||
|
"""]
|
||||||
|
|
||||||
|
relative_import_test_2 = [
|
||||||
|
"a.module",
|
||||||
|
["a", "a.module",
|
||||||
|
"a.sys",
|
||||||
|
"a.b", "a.b.y", "a.b.z",
|
||||||
|
"a.b.c", "a.b.c.d",
|
||||||
|
"a.b.c.e",
|
||||||
|
"a.b.c.moduleC",
|
||||||
|
"a.b.c.f",
|
||||||
|
"a.b.x",
|
||||||
|
"a.another"],
|
||||||
|
[], [],
|
||||||
|
"""\
|
||||||
|
mymodule.py
|
||||||
|
a/__init__.py
|
||||||
|
from . import sys # a.sys
|
||||||
|
a/another.py
|
||||||
|
a/module.py
|
||||||
|
from .b import y, z # a.b.y, a.b.z
|
||||||
|
a/exceptions.py
|
||||||
|
a/sys.py
|
||||||
|
a/b/__init__.py
|
||||||
|
from .c import moduleC # a.b.c.moduleC
|
||||||
|
from .c import d # a.b.c.d
|
||||||
|
a/b/x.py
|
||||||
|
a/b/y.py
|
||||||
|
a/b/z.py
|
||||||
|
a/b/c/__init__.py
|
||||||
|
from . import e # a.b.c.e
|
||||||
|
a/b/c/moduleC.py
|
||||||
|
#
|
||||||
|
from . import f # a.b.c.f
|
||||||
|
from .. import x # a.b.x
|
||||||
|
from ... import another # a.another
|
||||||
|
a/b/c/d.py
|
||||||
|
a/b/c/e.py
|
||||||
|
a/b/c/f.py
|
||||||
|
"""]
|
||||||
|
|
||||||
|
def open_file(path):
|
||||||
|
##print "#", os.path.abspath(path)
|
||||||
|
dirname = os.path.dirname(path)
|
||||||
|
distutils.dir_util.mkpath(dirname)
|
||||||
|
return open(path, "w")
|
||||||
|
|
||||||
|
def create_package(source):
|
||||||
|
ofi = None
|
||||||
|
for line in source.splitlines():
|
||||||
|
if line.startswith(" ") or line.startswith("\t"):
|
||||||
|
ofi.write(line.strip() + "\n")
|
||||||
|
else:
|
||||||
|
ofi = open_file(os.path.join(TEST_DIR, line.strip()))
|
||||||
|
|
||||||
|
class ModuleFinderTest(unittest.TestCase):
|
||||||
|
def _do_test(self, info, report=False):
|
||||||
|
import_this, modules, missing, maybe_missing, source = info
|
||||||
|
create_package(source)
|
||||||
|
try:
|
||||||
|
mf = modulefinder.ModuleFinder(path=TEST_PATH)
|
||||||
|
mf.import_hook(import_this)
|
||||||
|
if report:
|
||||||
|
mf.report()
|
||||||
|
## # This wouldn't work in general when executed several times:
|
||||||
|
## opath = sys.path[:]
|
||||||
|
## sys.path = TEST_PATH
|
||||||
|
## try:
|
||||||
|
## __import__(import_this)
|
||||||
|
## except:
|
||||||
|
## import traceback; traceback.print_exc()
|
||||||
|
## sys.path = opath
|
||||||
|
## return
|
||||||
|
modules = set(modules)
|
||||||
|
found = set(mf.modules.keys())
|
||||||
|
more = list(found - modules)
|
||||||
|
less = list(modules - found)
|
||||||
|
# check if we found what we expected, not more, not less
|
||||||
|
self.failUnlessEqual((more, less), ([], []))
|
||||||
|
|
||||||
|
# check for missing and maybe missing modules
|
||||||
|
bad, maybe = mf.any_missing_maybe()
|
||||||
|
self.failUnlessEqual(bad, missing)
|
||||||
|
self.failUnlessEqual(maybe, maybe_missing)
|
||||||
|
finally:
|
||||||
|
distutils.dir_util.remove_tree(TEST_DIR)
|
||||||
|
|
||||||
|
def test_package(self):
|
||||||
|
self._do_test(package_test)
|
||||||
|
|
||||||
|
def test_maybe(self):
|
||||||
|
self._do_test(maybe_test)
|
||||||
|
|
||||||
|
if getattr(__future__, "absolute_import", None):
|
||||||
|
|
||||||
|
def test_maybe_new(self):
|
||||||
|
self._do_test(maybe_test_new)
|
||||||
|
|
||||||
|
def test_absolute_imports(self):
|
||||||
|
self._do_test(absolute_import_test)
|
||||||
|
|
||||||
|
def test_relative_imports(self):
|
||||||
|
self._do_test(relative_import_test)
|
||||||
|
|
||||||
|
def test_relative_imports_2(self):
|
||||||
|
self._do_test(relative_import_test_2)
|
||||||
|
|
||||||
|
def test_main():
|
||||||
|
test_support.run_unittest(ModuleFinderTest)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
Loading…
Reference in New Issue