Cyclic imports

Barry Scott barry at barrys-emacs.org
Tue Aug 17 14:05:15 EDT 2021


On Monday, 16 August 2021 16:13:47 BST Dan Stromberg wrote:
> Hi folks.
> 
> I'm working on a large codebase that has at least one cyclic import.
> 
> In case I end up needing to eliminate the cyclic imports, is there any sort
> of tool that will generate an import graph and output Just the cycles?
> 
> I tried pyreverse, but it produced too big a graph to be very useful; it
> showed all internal imports, not just the cycles.

I wrote this code to track down a cycling import.
Note  it handles import module, but not from module import.
You would need to make a (simple) edit to add that.

---- py_import_time.py ---
#!/usr/bin/python3
import sys
import pathlib

class PyImportTree:
    def __init__( self, main_module, python_path ):
        self.main_module = main_module
        self.python_path = python_path

        self.all_modules = {}
        self.loadTree( self.main_module )

        self.all_being_imported = set()
        self.problem_imports = 0

    def loadTree( self, module_name ):
        all_imports = self.allImports( module_name )
        if all_imports is None:
            return

        self.all_modules[ module_name ] = all_imports

        for module in all_imports:
            if module not in self.all_modules:
                self.loadTree( module )

    def findModule( self, module_name ):
        for folder in self.python_path:
            abs_path = pathlib.Path( folder ) / ('%s.py' % (module_name,))
            if abs_path.exists():
                return abs_path

        return None

    def allImports( self, module_name ):
        all_imports = []
        filename = self.findModule( module_name )
        if filename is None:
            print( 'Error: Cannot find module %s' % (module_name,), 
file=sys.stderr )
            return None

        with open( str(filename), 'r' ) as f:
            for line in f:
                words = line.strip().split()
                if words[0:1] == ['import']:
                    all_imports.append( words[1] )

        return all_imports

    def printTree( self ):
        self.__printTree( self.main_module, 0 )
        if self.problem_imports > 0:
            print( '%d problem imports' % (self.problem_imports,), 
file=sys.stderr )

    def __printTree( self, module_name, indent ):
        if module_name not in self.all_modules:
            return

        if module_name in self.all_being_imported:
            print( '%s%s' % ('>   '*indent, module_name) )
            self.problem_imports += 1
            return

        print( '%s%s' % ('-   '*indent, module_name) )

        self.all_being_imported.add( module_name )

        for module in self.all_modules[ module_name ]:
            self.__printTree( module, indent+1 )

        self.all_being_imported.remove( module_name )

if __name__ == '__main__':
    sys.setrecursionlimit( 30 )
    sys.exit( PyImportTree( sys.argv[1], sys.argv[2:] ).printTree() )





More information about the Python-list mailing list