Detección de importaciones circulares.

Estoy trabajando con un proyecto que contiene alrededor de 30 módulos únicos. No se diseñó demasiado bien, por lo que es común que cree importaciones circulares al agregar alguna funcionalidad nueva al proyecto.

Por supuesto, cuando agrego la importación circular, no la conozco. A veces es bastante obvio que hice una importación circular cuando recibí un error como AttributeError: 'module' object has no attribute 'attribute' donde definí claramente 'attribute' . Pero otras veces, el código no lanza excepciones debido a la forma en que se usa.

Por lo tanto, a mi pregunta:

¿Es posible detectar mediante progtwigción cuándo y dónde se está produciendo una importación circular?

La única solución que puedo pensar hasta ahora es tener un módulo importTracking que contenga un dict importInProgress(file) , una función importInProgress(file) , que incremente importingModules[file] y arroje un error si es mayor que 1, y una función importComplete(file) que disminuye importingModules[file] . Todos los otros módulos se verían como:

 import importTracking importTracking.importInProgress(__file__) #module code goes here. importTracking.importComplete(__file__) 

Pero eso parece realmente desagradable, tiene que haber una mejor manera de hacerlo, ¿verdad?

Para evitar tener que alterar todos los módulos, puede pegar su funcionalidad de seguimiento de importación en un gancho de importación , o en una __import__ personalizada puede pegar en los incorporados; esto último, por una vez, podría funcionar mejor, porque se llama a __import__ incluso si el módulo que se está importando ya está en sys.modules , que es el caso durante las importaciones circulares.

Para la implementación, simplemente usaría un conjunto de los módulos “en el proceso de importación”, algo así como (edición de edición: Insertando un fragmento de trabajo derivado del 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 

No todas las importaciones circulares son un problema, como se ha encontrado cuando no se lanza una excepción.

Cuando son un problema, obtendrás una excepción la próxima vez que intentes ejecutar cualquiera de tus pruebas. Puedes cambiar el código cuando esto suceda.

No veo ningún cambio requerido de esta situación.

Ejemplo de cuando no es un problema:

a.py

 import b a = 42 def f(): return bb 

b.py

 import a b = 42 def f(): return aa 

import utiliza __builtin__.__import__() , por lo tanto, si hace un monkeypatch, entonces cada importación en todas partes recogerá los cambios. Tenga en cuenta que una importación circular no es necesariamente un problema.

Las importaciones circulares en Python no son como PHP incluye.

Los módulos importados de Python se cargan la primera vez en un “controlador” de importación y se guardan allí durante la duración del proceso. Este controlador asigna nombres en el espacio de nombres local para lo que se importe de ese módulo, para cada importación posterior. Un módulo es único, y una referencia a ese nombre del módulo siempre apuntará al mismo módulo cargado, independientemente de dónde se importó.

Por lo tanto, si tiene una importación de módulo circular, la carga de cada archivo se realizará una vez, y luego cada módulo tendrá nombres relacionados con el otro módulo creado en su espacio de nombres.

Por supuesto, podría haber problemas al referirse a nombres específicos dentro de ambos módulos (cuando se producen las importaciones circulares ANTES de las definiciones de clase / función a las que se hace referencia en las importaciones de los módulos opuestos), pero obtendrá un error si eso sucede.