Scripted module dependency loaded too late for one module but for another one not

From: Christian Herz / http://slicer-devel.65872.n3.nabble.com/Scripted-module-dependency-loaded-too-late-for-one-module-but-for-another-one-not-tt4038485.html

Hi developers,

I noticed that there is something wrong with the order of when which dependency is loaded. It’s either a problem with my extensions or Slicer. Not sure about this.

We have a module Quantitative Reporting [1] which depends (not much longer) on SlicerProstate [2]. I have been working on factoring out some code into another repository and I noticed that the dependency is loaded to late into Slicer (even when I compile the python scripted module).

Here an example for reproduction:

  1. Execute the following in a directory that you want to use for testing this issue:

    git clone https://github.com/QIICR/QuantitativeReporting.git
    git clone https://github.com/SlicerProstate/mpReview.git
    git clone https://github.com/SlicerProstate/SlicerProstate.git
    
  2. Add the top-level directory of QuantitativeReporting to your current Slicer

  3. Compile SlicerProstate

  4. Add the {SlicerProstate_Build_Directory}/lib/Slicer4.7/qt-scripted-modules

  5. Restart Slicer

—> Everything should be fine. No errors visible in python console

Now try this:

  1. Delete {SlicerProstate_Build_Directory}/lib/Slicer4.7/qt-scripted-modules or delete DistanceMapBasedRegistration.py and skip steps 2-5.
  2. Open CMakeLists.txt in toplevel directory of SlicerProstate for editing
  3. Delete the following lines 21-24:
add_subdirectory(QuadEdgeSurfaceMesher) 
add_subdirectory(SegmentationSmoothing) 
add_subdirectory(DistanceMapBasedRegistration) 
add_subdirectory(DWModeling)
  1. Compile SlicerProstate
  2. Add the {SlicerProstate_Build_Directory}/lib/Slicer4.7/qt-scripted-modules
  3. Restart Slicer
>>> Traceback (most recent call last):
File "<string>", line 1, in <module>
File "/Users/christian/sources/py/Reporting/QuantitativeReporting.py", line 8, in <module>
from SlicerProstateUtils.mixins import *
ImportError: No module named SlicerProstateUtils.mixins

When I add mpReview[3] (both depend on SlicerProstate) I only get errors from QuantitativeReporting but not form mpReview displayed.

I am not sure about the cause, but I assume that the cause why it’s is properly loaded in the first scenario is that DistanceMapBasedRegistration.py coexists next to SlicerProstate.py.

Hope someone can help me with that issue.

Thanks,
Christian

[1] https://github.com/QIICR/QuantitativeReporting
[2] https://github.com/SlicerProstate/SlicerProstate
[3] https://github.com/SlicerProstate/mpReview

@che85 wow, I have to say I had to re-read this many times!

I don’t know what’s going on, but maybe the problem can be narrowed down a bit better:

  • I think it would be more consistent (and that is how the factories work) to build each of the extensions, and add paths to the built directories for all of the modules

  • can you reproduce the problem if you just take SlicerProstate, and then try to import from python console SlicerProstateUtils.mixins first compiling SlicerProstate with DistanceMapRegistration “added”, and then cleaning it up and building it without that directory?

It is a bit difficult to follow because of many pieces involved.

I thought about this to, but this would make it even more complicated, right?

I don’t know how dependencies are resolved right now when starting Slicer, but when there is a dependency defined for a module and the dependency hasn’t been loaded yet, the load process of that module should be moved to the end of the list for modules that need to be loaded. In the end if the dependency still couldn’t be resolved, it should simply throw an error.

Maybe JC can tell us more about that loading process?

I think it would be more consistent (and that is how the factories work) to build each of the extensions, and add paths to the built directories for all of the modules

Loading SlicerProstateUtils.mixins from the python console works in any case.

can you reproduce the problem if you just take SlicerProstate, and then try to import from python console SlicerProstateUtils.mixins first compiling SlicerProstate with DistanceMapRegistration “added”, and then cleaning it up and building it without that directory?

Ok here a quick update for simpler reproduction. Thanks to @fedorov:

  1. Download and install Slicer nightly build from http://download.slicer.org/

  2. Open Extension Manager

  3. Install extensions Quantitative Reporting and mpReview

  4. Restart Slicer
    When you open the Python console, no errors should be displayed.

  5. Go to directory {Slicer_Install_DIR}/Extensions-#####/SlicerProstate/lib/Slicer4.7/qt-scripted-modules extension

  6. Delete DistanceMapBasedRegistration.py and DistanceMapBasedRegistration.pyc

  7. Restart Slicer
    Notice the error in the python console

1 Like

@che85 there are multiple stages in the module initialization process: registration, instantiation, and loading. Instantiation is when the scripted module is parsed, so any import dependencies must be resolveable at that time. However, instantiation is done alphabetically, with no dependency resolution – because the module factory does not know anything about dependencies (the module itself supplies the dependencies function). QuantitativeReporting works “by accident” when you have DistanceMapBasedRegistration in place because DMBR is in the same directory where SlicerProstateUtils is installed, so that the parent directory is added to PYTHONPATH before attempting to instantiate QuantitativeReporting (During instantiation, the path for each module is added to PYTHONPATH). When you remove it, SlicerProstate comes later alphabetically, so the path is not available when QR is instantiated – as @fedorov noted, the import works fine in the python console once Slicer starts.

Anyway, there’s probably a few ways this could be fixed, including:

  • add all the additionalModulePaths to PYTHONPATH (not sure what this would break, there’s a lot of existing logic for adding to PYTHONPATH in various places, including for regular loadable modules).
  • add the path during registration rather than instantiation (simplest fix I can think of). Try this patch:
patch to test
diff --git a/Base/QTGUI/qSlicerScriptedLoadableModuleFactory.cxx b/Base/QTGUI/qSlicerScriptedLoadableModuleFactory.cxx
index 9ca65a503..939160247 100644
--- a/Base/QTGUI/qSlicerScriptedLoadableModuleFactory.cxx
+++ b/Base/QTGUI/qSlicerScriptedLoadableModuleFactory.cxx
@@ -43,6 +43,24 @@
 //----------------------------------------------------------------------------
 bool ctkFactoryScriptedItem::load()
 {
+#ifdef Slicer_USE_PYTHONQT
+  if (!qSlicerCoreApplication::testAttribute(qSlicerCoreApplication::AA_DisablePython))
+    {
+    // By convention, if the module is not embedded, "<MODULEPATH>" will be appended to PYTHONPATH
+    if (!qSlicerCoreApplication::application()->isEmbeddedModule(this->path()))
+      {
+      QDir modulePathWithoutIntDir = QFileInfo(this->path()).dir();
+      QString intDir = qSlicerCoreApplication::application()->intDir();
+      if (intDir ==  modulePathWithoutIntDir.dirName())
+        {
+        modulePathWithoutIntDir.cdUp();
+        }
+      qSlicerCorePythonManager * pythonManager = qSlicerCoreApplication::application()->corePythonManager();
+      pythonManager->appendPythonPaths(QStringList() << modulePathWithoutIntDir.absolutePath());
+      }
+    }
+#endif
+
   return true;
 }

@@ -59,24 +77,6 @@ qSlicerAbstractCoreModule* ctkFactoryScriptedItem::instanciator()
   module->setInstalled(qSlicerUtils::isPluginInstalled(this->path(), app->slicerHome()));
   module->setBuiltIn(qSlicerUtils::isPluginBuiltIn(this->path(), app->slicerHome()));

-#ifdef Slicer_USE_PYTHONQT
-  if (!qSlicerCoreApplication::testAttribute(qSlicerCoreApplication::AA_DisablePython))
-    {
-    // By convention, if the module is not embedded, "<MODULEPATH>" will be appended to PYTHONPATH
-    if (!qSlicerCoreApplication::application()->isEmbeddedModule(module->path()))
-      {
-      QDir modulePathWithoutIntDir = QFileInfo(module->path()).dir();
-      QString intDir = qSlicerCoreApplication::application()->intDir();
-      if (intDir ==  modulePathWithoutIntDir.dirName())
-        {
-        modulePathWithoutIntDir.cdUp();
-        }
-      qSlicerCorePythonManager * pythonManager = qSlicerCoreApplication::application()->corePythonManager();
-      pythonManager->appendPythonPaths(QStringList() << modulePathWithoutIntDir.absolutePath());
-      }
-    }
-#endif
-
   bool ret = module->setPythonSource(this->path());
   if (!ret)
     {

Related:
http://www.na-mic.org/Bug/view.php?id=4242
http://www.na-mic.org/Bug/view.php?id=4051

2 Likes

@ihnorton, @fedorov I applied the patch and compiled Slicer under Ubuntu and indeed it works! I don’t get those errors anymore. When can I expect this to be available in the nightly?

Thanks a lot.
Christian

@ihnorton Good catch. Moving the update of python path at load time makes sense.

Since I got :thumbsup: from Jc I’ll commit tomorrow if there’s no objection.

https://github.com/Slicer/Slicer/pull/705

4 Likes