Commits

Ronald Oussoren committed 278b6a4

Fix issue with finding the correct modules when the __init__ of a package contains import statements.

Without this patch modulegraph creates incomplete graphs for SQLalchemy (not
to pick on that project, I found the issue in modulegraph while researching
a mailing-list question about py2app support for SQLAlchemy).

Comments (0)

Files changed (9)

doc/changelog.rst

 * ``modulegraph.util.imp_walk`` is deprecated and will be 
   removed in the next release of this package.
 
+Bugfixes
+........
+
+* The module graph was incomplete, and generated incorrect warnings
+  along the way, when a subpackage contained import statements for
+  submodules.
+
+  An example of this is ``sqlalchemy.util``, the ``__init__.py`` file
+  for this package contains imports of modules in that modules using
+  the classic relative import syntax (that is ``import compat`` to
+  import ``sqlalchemy.util.compat``). Until this release modulegraph
+  searched the wrong path to locate these modules (and hence failed
+  to find them).
+
+
 0.9.2
 -----
 

modulegraph/modulegraph.py

         if caller:
             pname = caller.identifier
 
-            if '.' in pname:
+            if isinstance(caller, Package):
+                parent = caller
+
+            elif '.' in pname:
                 pname = pname[:pname.rfind('.')]
                 parent = self.findNode(pname)
 
 
         
 
-        if not pkgpath:
-            # XXX: pkgpath is only set for PEP420 packages, which don't have
-            # an __init__ module. Need a cleaner interface for this.
+        try:
+            self.msg(2, "find __init__ for %s"%(m.packagepath,))
+            fp, buf, stuff = self.find_module("__init__", m.packagepath)
+        except ImportError:
+            pass
 
-            fp, buf, stuff = self.find_module("__init__", m.packagepath)
+        else:
             try:
+                self.msg(2, "load __init__ for %s"%(m.packagepath,))
                 self.load_module(fqname, fp, buf, stuff)
             finally:
                 if fp is not None:

modulegraph_tests/test_import_from_init.py

+import unittest
+import sys
+import textwrap
+import subprocess
+import os
+from modulegraph import modulegraph
+
+class TestNativeImport (unittest.TestCase):
+    # The tests check that Python's import statement
+    # works as these tests expect.
+
+    def importModule(self, name):
+        if '.' in name:
+            script = textwrap.dedent("""\
+                try:
+                    import %s
+                except ImportError:
+                    import %s
+                print (%s.__name__)
+            """) %(name, name.rsplit('.', 1)[0], name)
+        else:
+            script = textwrap.dedent("""\
+                import %s
+                print (%s.__name__)
+            """) %(name, name)
+
+        p = subprocess.Popen([sys.executable, '-c', script], 
+                stdout=subprocess.PIPE,
+                stderr=subprocess.STDOUT,
+                cwd=os.path.join(
+                    os.path.dirname(os.path.abspath(__file__)),
+                    'testpkg-import-from-init'),
+        )
+        data = p.communicate()[0]
+        if sys.version_info[0] != 2:
+            data = data.decode('UTF-8')
+        data = data.strip()
+
+        if data.endswith(' refs]'):
+            # with --with-pydebug builds
+            data = data.rsplit('\n', 1)[0].strip()
+
+        sts = p.wait()
+
+        if sts != 0:
+            print (data)
+        self.assertEqual(sts, 0)
+        return data
+        
+
+    def testRootPkg(self):
+        m = self.importModule('pkg')
+        self.assertEqual(m, 'pkg')
+
+    def testSubPackage(self):
+        m = self.importModule('pkg.subpkg')
+        self.assertEqual(m, 'pkg.subpkg')
+
+    
+class TestModuleGraphImport (unittest.TestCase):
+    if not hasattr(unittest.TestCase, 'assertIsInstance'):
+        def assertIsInstance(self, value, types):
+            if not isinstance(value, types):
+                self.fail("%r is not an instance of %r"%(value, types))
+
+    def setUp(self):
+        root = os.path.join(
+                os.path.dirname(os.path.abspath(__file__)),
+                'testpkg-import-from-init')
+        self.mf = modulegraph.ModuleGraph(path=[ root ] + sys.path)
+        #self.mf.debug = 999
+        self.mf.run_script(os.path.join(root, 'script.py'))
+
+
+    def testRootPkg(self):
+        node = self.mf.findNode('pkg')
+        self.assertIsInstance(node, modulegraph.Package)
+        self.assertEqual(node.identifier, 'pkg')
+
+    def testSubPackage(self):
+        node = self.mf.findNode('pkg.subpkg')
+        self.assertIsInstance(node, modulegraph.Package)
+        self.assertEqual(node.identifier, 'pkg.subpkg')
+
+        node = self.mf.findNode('pkg.subpkg.compat')
+        self.assertIsInstance(node, modulegraph.SourceModule)
+        self.assertEqual(node.identifier, 'pkg.subpkg.compat')
+
+        node = self.mf.findNode('pkg.subpkg._collections')
+        self.assertIsInstance(node, modulegraph.SourceModule)
+        self.assertEqual(node.identifier, 'pkg.subpkg._collections')
+
+
+if __name__ == "__main__":
+    unittest.main()

modulegraph_tests/test_modulegraph.py

         self.assertEqual(graph.determine_parent(m), m)
 
         m = graph.findNode('xml.dom')
-        self.assertEqual(graph.determine_parent(m), graph.findNode('xml'))
+        self.assertEqual(graph.determine_parent(m), graph.findNode('xml.dom'))
 
 
     @expectedFailure

modulegraph_tests/testpkg-import-from-init/pkg/__init__.py

+""" pkg """

modulegraph_tests/testpkg-import-from-init/pkg/subpkg/__init__.py

+""" pkg.subpkg """
+
+from compat import X, Y
+
+from _collections import A, B

modulegraph_tests/testpkg-import-from-init/pkg/subpkg/_collections.py

+""" pkg.subpkg._collections """
+
+A, B = "A", "B"

modulegraph_tests/testpkg-import-from-init/pkg/subpkg/compat.py

+""" pkg.subpkg.compat """
+
+X, Y = 1, 2

modulegraph_tests/testpkg-import-from-init/script.py

+import pkg.subpkg