Skip to main content

Lib/modulefinder.py

cpython 3.14 @ ab2d84fe1023/Lib/modulefinder.py

modulefinder.py provides ModuleFinder, a class that statically traces every module a script imports. Rather than running the script, it disassembles each .pyc file (or compiles the .py on the fly) and looks for IMPORT_NAME and IMPORT_FROM opcodes. It then follows those imports recursively, building up a modules dict that maps module names to Module objects.

The tracer predates importlib and does not use the import machinery at runtime. It walks sys.path (or a caller-supplied path list) itself, honours __path__ for packages, and handles relative imports by tracking the parent package context. Missing modules end up in BADMODULES, a set that callers can inspect to detect broken dependencies before packaging or freezing an application. py2exe, cx_Freeze, and similar bundlers built on this foundation.

Two module-level helpers let callers influence path resolution before creating a ModuleFinder instance. AddPackagePath(pkg, path) appends an extra search directory for a named package, mirroring .pth file effects. ReplacePackage(oldname, newname) aliases one module name to another, which is useful when platform-specific packages shadow pure-Python ones during static analysis.

Map

LinesSymbolRolegopy
1-50imports, constantsBADMODULES, LOAD_CONST/IMPORT_NAME opcode constantsn/a
51-80AddPackagePath, ReplacePackageglobal path and alias helpersnot ported
81-180Modulelightweight container: __name__, __file__, __path__not ported
181-350ModuleFinder.__init__, run_scriptinitialise search paths, kick off recursive scannot ported
351-460import_module, load_module, scan_codebytecode disassembly and recursive import followingnot ported
461-530report, __main__ guardpretty-print results and CLI entry pointnot ported

Reading

Global path helpers (lines 51 to 80)

cpython 3.14 @ ab2d84fe1023/Lib/modulefinder.py#L51-80

AddPackagePath and ReplacePackage mutate two module-level dicts (packagePathMap and replacePackage). ModuleFinder.__init__ consults these dicts when building the initial search-path list, so callers must invoke them before constructing the finder. This design predates the importlib.machinery metadata API.

def AddPackagePath(packagename, path):
packagePathMap.setdefault(packagename, []).append(path)

def ReplacePackage(oldname, newname):
replacePackage[oldname] = newname

Module container (lines 81 to 180)

cpython 3.14 @ ab2d84fe1023/Lib/modulefinder.py#L81-180

Module is a lightweight namespace object. It stores __name__, __file__, __path__ (a list for packages, None for plain modules), and a globalnames dict that records every name the module assigns at module scope. The globalnames dict lets scan_code resolve from pkg import * without executing code.

class Module:
def __init__(self, name, file=None, path=None):
self.__name__ = name
self.__file__ = file
self.__path__ = path
self.globalnames = {}

run_script and initialisation (lines 181 to 350)

cpython 3.14 @ ab2d84fe1023/Lib/modulefinder.py#L181-350

ModuleFinder.__init__ accepts a path list (default sys.path), a debug level, an excludes list, and a replace_paths list for source-root rewriting. run_script(pathname) compiles the entry-point file with compile(), wraps it in a synthetic Module("__main__"), and calls scan_code() to begin the recursive walk. The modules dict accumulates every module seen; badmodules records every name that could not be resolved.

def run_script(self, pathname):
self.msg(2, "run_script", pathname)
with open(pathname, "rb") as fp:
stuff = ("", "rb", imp.PY_SOURCE)
self.load_module("__main__", fp, pathname, stuff)

Bytecode scanning (lines 351 to 460)

cpython 3.14 @ ab2d84fe1023/Lib/modulefinder.py#L351-460

scan_code(co, m) iterates over the code object's co_consts list recursively (to handle nested functions and classes) and steps through the opcode stream looking for IMPORT_NAME. When it finds one it calls import_module(), passing the name, the fromlist taken from the preceding LOAD_CONST, and the current package context for relative-import resolution. import_module() handles the replacePackage alias map and delegates to load_module() for the actual file search.

for opcode, oparg in zip(...):
if opcode == IMPORT_NAME:
name = co.co_names[oparg]
self.import_module(name, fqname, m)

report() and CLI (lines 461 to 530)

cpython 3.14 @ ab2d84fe1023/Lib/modulefinder.py#L461-530

report() prints three sections to stdout: modules found (with file paths), modules that were missing, and modules that raised errors during loading. The __main__ guard at the bottom accepts a script path as sys.argv[1] and calls run_script() then report(), making the module usable as a quick command-line dependency auditor.

def report(self):
print(" %-25s %s" % ("Name", "File"))
print(" %-25s %s" % ("----", "----"))
for key in keys:
m = self.modules[key]
print(" %-25s %s" % (key, m.__file__ or ""))

gopy mirror

Not yet ported.