I'm working with a project that contains about 30 unique modules. It wasn't designed too well, so it's common that I create circular imports when adding some new functionality to the project.
Of course, when I add the circular import, I'm unaware of it. Sometimes it's pretty obvious I've made a circular import when I get an error like AttributeError: 'module' object has no attribute 'attribute'
where I clearly defined 'attribute'
. But other times, the code doesn't throw exceptions because of the way it's used.
So, to my question:
Is it possible to programmatically detect when and where a circular import is occuring?
The only solution I can think of so far is to have a module importTracking
that contains a dict importingModules
, a function importInProgress(file)
, which increments importingModules[file]
, and throws an error if it's greater than 1, and a function importComplete(file)
which decrements importingModules[file]
. All other modules would look like:
import importTracking importTracking.importInProgress(__file__) #module code goes here. importTracking.importComplete(__file__)
But that looks really nasty, there's got to be a better way to do it, right?
Generally, the Python Circular Import problem occurs when you accidentally name your working file the same as the module name and those modules depend on each other. This way the python opens the same file which causes a circular loop and eventually throws an error.
You can, however, use the imported module inside functions and code blocks that don't get run on import. Generally, in most valid cases of circular dependencies, it's possible to refactor or reorganize the code to prevent these errors and move module references inside a code block.
By running a cli command npx madge --circular --extensions ts ./ we can quickly get a list of circular dependencies of all . ts files in current directory and its subdirectories.
To avoid having to alter every module, you could stick your import-tracking functionality in a import hook, or in a customized __import__
you could stick in the built-ins -- the latter, for once, might work better, because __import__
gets called even if the module getting imported is already in sys.modules
, which is the case during circular imports.
For the implementation I'd simply use a set of the modules "in the process of being imported", something like (benjaoming edit: Inserting a working snippet derived from original):
beingimported = set() originalimport = __import__ def newimport(modulename, *args, **kwargs): if modulename in beingimported: print "Importing in circles", modulename, args, kwargs print " Import stack trace -> ", beingimported # sys.exit(1) # Normally exiting is a bad idea. beingimported.add(modulename) result = originalimport(modulename, *args, **kwargs) if modulename in beingimported: beingimported.remove(modulename) return result import __builtin__ __builtin__.__import__ = newimport
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With