Repository: pyinstaller/pyinstaller

Issue:
importing tifffile causes RecursionError
> Pyinstaller 3.3 and 3.4currentdev
> Anaconda 4.3.27
> Python 3.6
> tifffile 0.12.1

**minimalTest.py**
`from tifffile import TiffFile`

**Make exe:**
`pyinstaller minimalTest.py`


**Workaround**
In *tifffile.py*:
Replace 

```
try:
    if __package__:
        from . import _tifffile  # noqa
    else:
        import _tifffile  # noqa
except ImportError:
    warnings.warn(
        "ImportError: No module named '_tifffile'. "
        "Loading of some compressed images will be very slow. "
        "Tifffile.c can be obtained at http://www.lfd.uci.edu/~gohlke/")
```
with
`from . import _tifffile`

**Error message:**
231 INFO: PyInstaller: 3.4.dev0+133d18156
231 INFO: Python: 3.6.1
232 INFO: Platform: Windows-10-10.0.10240-SP0
237 INFO: wrote D:\Programming\eclipse_workspace\scripts\AAA\test.spec
10504 INFO: UPX is not available.
10508 INFO: Extending PYTHONPATH with paths
['D:\\Programming\\eclipse_workspace\\scripts',
 'D:\\Programming\\eclipse_workspace\\scripts\\AAA']
10508 INFO: checking Analysis
10508 INFO: Building Analysis because out00-Analysis.toc is non existent
10508 INFO: Initializing module dependency graph...
10514 INFO: Initializing module graph hooks...
10521 INFO: Analyzing base_library.zip ...
21421 INFO: running Analysis out00-Analysis.toc
21426 INFO: Adding Microsoft.Windows.Common-Controls to dependent assemblies of final executable
  required by C:\ProgramData\Anaconda3\python.exe
22475 INFO: Caching module hooks...
22482 INFO: Analyzing D:\Programming\eclipse_workspace\scripts\AAA\test.py
27173 INFO: Processing pre-find module path hook   distutils
33490 INFO: Processing pre-find module path hook   site
33493 INFO: site: retargeting to fake-dir 'C:\\ProgramData\\Anaconda3\\lib\\site-packages\\PyInstaller\\fake-modules'
Traceback (most recent call last):
  File "C:\ProgramData\Anaconda3\Scripts\pyinstaller-script.py", line 11, in <module>
    load_entry_point('PyInstaller==3.4.dev0+133d18156', 'console_scripts', 'pyinstaller')()
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\__main__.py", line 92, in run
    run_build(pyi_config, spec_file, **vars(args))
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\__main__.py", line 46, in run_build
    PyInstaller.building.build_main.main(pyi_config, spec_file, **kwargs)
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\building\build_main.py", line 791, in main
    build(specfile, kw.get('distpath'), kw.get('workpath'), kw.get('clean_build'))
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\building\build_main.py", line 737, in build
    exec(text, spec_namespace)
  File "<string>", line 16, in <module>
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\building\build_main.py", line 213, in __init__
    self.__postinit__()
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\building\datastruct.py", line 161, in __postinit__
    self.assemble()
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\building\build_main.py", line 415, in assemble
    priority_scripts.append(self.graph.run_script(script))
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\depend\analysis.py", line 201, in run_script
    self._top_script_node = super(PyiModuleGraph, self).run_script(pathname)
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 1343, in run_script
    self._scan_code(m, co, co_ast)
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 2528, in _scan_code
    self._process_imports(module)
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 2727, in _process_imports
    target_module = self._safe_import_hook(*import_info, **kwargs)[0]
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 2202, in _safe_import_hook
    target_attr_names=None, level=level, edge_attr=edge_attr)
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 1416, in import_hook
    source_package, target_module_partname, level)
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 1568, in _find_head_package
    target_module_headname, target_package_name, source_package)
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\depend\analysis.py", line 271, in _safe_import_module
    module_basename, module_name, parent_package)
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 1985, in _safe_import_module
    module_name, file_handle, pathname, metadata)
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 2023, in _load_module
    m = self._load_package(fqname, pathname, packagepath)
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 2797, in _load_package
    self._load_module(fqname, fp, buf, stuff)
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 2077, in _load_module
    self._scan_code(m, co, co_ast)
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 2528, in _scan_code
    self._process_imports(module)
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 2727, in _process_imports
    target_module = self._safe_import_hook(*import_info, **kwargs)[0]
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 2202, in _safe_import_hook
    target_attr_names=None, level=level, edge_attr=edge_attr)
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 1416, in import_hook
    source_package, target_module_partname, level)
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 1568, in _find_head_package
    target_module_headname, target_package_name, source_package)
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\depend\analysis.py", line 271, in _safe_import_module
    module_basename, module_name, parent_package)
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 1985, in _safe_import_module
    module_name, file_handle, pathname, metadata)
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 2077, in _load_module
    self._scan_code(m, co, co_ast)
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 2528, in _scan_code
    self._process_imports(module)
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 2727, in _process_imports
    target_module = self._safe_import_hook(*import_info, **kwargs)[0]
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 2312, in _safe_import_hook
    target_attr_names=None, level=level, edge_attr=edge_attr)
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 2312, in _safe_import_hook
    target_attr_names=None, level=level, edge_attr=edge_attr)
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 2312, in _safe_import_hook
    target_attr_names=None, level=level, edge_attr=edge_attr)
  [Previous line repeated 943 more times]
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 2202, in _safe_import_hook
    target_attr_names=None, level=level, edge_attr=edge_attr)
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 1416, in import_hook
    source_package, target_module_partname, level)
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 1568, in _find_head_package
    target_module_headname, target_package_name, source_package)
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\depend\analysis.py", line 271, in _safe_import_module
    module_basename, module_name, parent_package)
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 1979, in _safe_import_module
    module_partname, search_dirs, parent_module)
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 2853, in _find_module
    return self._find_module_path(fullname, name, path)
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\depend\analysis.py", line 315, in _find_module_path
    fullname, module_name, search_dirs)
  File "C:\ProgramData\Anaconda3\lib\site-packages\PyInstaller\lib\modulegraph\modulegraph.py", line 2934, in _find_module_path
    module_name)
  File "<frozen importlib._bootstrap_external>", line 1223, in find_loader
  File "<frozen importlib._bootstrap_external>", line 1268, in find_spec
  File "<frozen importlib._bootstrap_external>", line 60, in _path_join
RecursionError: maximum recursion depth exceeded
Infinite recursion in "import decimal"
With Ubuntu 14.04 Python 3.4 and latest pyinstaller (3.4.dev0+d258d2882), running pyinstaller on a file containing "import decimal" fails. The reason is that this system distribution of Python doesn't have a "_decimal" module for whatever reason. decimal tries to import _decimal and fails gracefully if it fails. Pyinstaller thinks _decimal is some kind of SWIG candidate, however, and so retries to import it over and over, eventually running off the end of the stack.


Discussion:
Same problem here. 

Gets stuck in an infinite recursive loop involving is_swig_candidate() and _safe_import_hook() being called over and over again on _tifffile

in my case, this is using scikit-image which includes tifffile. This only occurs on scikit-image 0.13. Downgrading to 0.12.3 solves the problem

Linking to what @radjkarl mentioned as workaround, the import at 0.12.3 is:
```
try:
    from . import _tifffile
except ImportError:
    warnings.warn(
        "failed to import the optional _tifffile C extension module.\n"
        "Loading of some compressed images will be slow.\n"
        "Tifffile.c can be obtained at http://www.lfd.uci.edu/~gohlke/")
```
while in 0.13 is:
```
try:
    if __package__:
        from . import _tifffile
    else:
        import _tifffile
except ImportError:
    warnings.warn(
        "ImportError: No module named '_tifffile'. "
        "Loading of some compressed images will be very slow. "
        "Tifffile.c can be obtained at http://www.lfd.uci.edu/~gohlke/")
```
I believe the pain point is here:
https://github.com/pyinstaller/pyinstaller/blob/133d1815643cda8efc99118e0ccb28a5ff4cf8c2/PyInstaller/lib/modulegraph/modulegraph.py#L2277-L2288
when tries to import _tifffile as a SWIG module and fails, ceates a MissingModule. then at 
https://github.com/pyinstaller/pyinstaller/blob/133d1815643cda8efc99118e0ccb28a5ff4cf8c2/PyInstaller/lib/modulegraph/modulegraph.py#L2298-L2315
It checks if it is a potential SWIG module and if it is a MissingModule and calls _safe_hook_import again, going through the same process over and over again
Thanks for you analysis. To verify this, please revert commit 711e9e77c93a979a63648ba05f725b30dbb7c3cc (which is the one introducing this behaviour).
I have the following problems.
 - importing tifffile causes RecursionError

Is there a solution?

The development environment is as follows.
Pyinstaller 3.3
Python 3.6
tifffile 0.12.1
See https://github.com/pyinstaller/pyinstaller/issues/2911#issuecomment-343203156. I still did not get an answer.
@radjkarl @dlejarazu I'm still waiting for an answer. Please revert commit 711e9e77c93a979a63648ba05f725b30dbb7c3cc and report back whether this solves the issue. Otherwise I'm going to close the issue.
@htgoebel sorry . dont have time for checking our the code reverting etc. 
My workaround resolved the issue.
To verify this, please revert commit 711e9e77c93a979a63648ba05f725b30dbb7c3cc (which is the one introducing this behaviour). Try is this solve the issue and report back here. Thanks.
I tried reproducing this (by moving _decimal.so out of the way), but this error is not triggered here. So I nee you help to track this down. Please do the following:
1. Add these lines to the top of PyInstaller/lib/modulegraph/modulegraph.py  -> `is_swig_candidate()` (should be line 2183):
```
            if target_module_partname == "_decimal":
                import pdb ; pdb.set_trace()
```
2. Run pyinstaller
3. When the debugger steps in, check the values, which are checked in `is_swig_candidate()`, like this
```
(Pdb) p source_module
SourceModule('decimal', '/usr/lib64/python3.5/decimal.py')
(Pdb) p target_attr_names
[]
(Pdb) p level, ABSOLUTE_IMPORT_LEVEL
(0, 0)
(Pdb) p type(source_module)
<class 'PyInstaller.lib.modulegraph.modulegraph.SourceModule'>
(Pdb) p target_module_partname, '_' + source_module.identifier.rpartition('.')[2]
('_decimal', '_decimal')
(Pdb) p sys.version_info[0]
3
```
4. Report what differs on your system form the values shown above. Thanks.

Resolving patch:
diff --git a/PyInstaller/lib/modulegraph/modulegraph.py b/PyInstaller/lib/modulegraph/modulegraph.py
--- a/PyInstaller/lib/modulegraph/modulegraph.py
+++ b/PyInstaller/lib/modulegraph/modulegraph.py
@@ -2188,7 +2188,20 @@ def is_swig_candidate():
                     target_module_partname ==
                       '_' + source_module.identifier.rpartition('.')[2] and
                     sys.version_info[0] == 3)
-            
+
+        def is_swig_wrapper(source_module):
+            # TODO Define a new function util.open_text_file() performing
+            # this logic, which is repeated numerous times in this module.
+            # FIXME: Actually, can't we just use the new compat.open()
+            # function to reliably open text files in a portable manner?
+            with open(source_module.filename, 'rb') as source_module_file:
+                encoding = util.guess_encoding(source_module_file)
+            with open(source_module.filename, _READ_MODE, encoding=encoding) \
+                    as source_module_file:
+                first_line = source_module_file.readline()
+            self.msg(5, 'SWIG wrapper candidate first line: %r' % (first_line))
+            return "automatically generated by SWIG" in first_line
+
 
         # List of the graph nodes created for all target modules both
         # imported by and returned from this call, whose:
@@ -2200,8 +2213,9 @@ def is_swig_candidate():
         target_modules = None
 
         # True if this is a Python 2-style implicit relative import of a
-        # SWIG-generated C extension.
-        is_swig_import = False
+        # SWIG-generated C extension. False if we checked and it is not SWIG.
+        # None if we haven't checked yet.
+        is_swig_import = None
 
         # Attempt to import this target module in the customary way.
         try:
@@ -2245,41 +2259,28 @@ def is_swig_candidate():
             # candidates. All other node types are safely ignorable.
             if is_swig_candidate():
                 self.msg(4, 'SWIG import candidate (name=%r, caller=%r, level=%r)' % (target_module_partname, source_module, level))
-
-                # TODO Define a new function util.open_text_file() performing
-                # this logic, which is repeated numerous times in this module.
-                # FIXME: Actually, can't we just use the new compat.open()
-                # function to reliably open text files in a portable manner?
-                with open(source_module.filename, 'rb') as source_module_file:
-                    encoding = util.guess_encoding(source_module_file)
-                with open(
-                    source_module.filename, _READ_MODE, encoding=encoding) as (
-                    source_module_file):
-                    first_line = source_module_file.readline()
-                    self.msg(5, 'SWIG import candidate shebang: %r' % (first_line))
-
-                    if "automatically generated by SWIG" in first_line:
-                        is_swig_import = True
-
-                        # Convert this Python 2-compliant implicit relative
-                        # import prohibited by Python 3 into a Python
-                        # 3-compliant explicit relative "from"-style import for
-                        # the duration of this function call by overwriting the
-                        # original parameters passed to this call.
-                        target_attr_names = [target_module_partname]
-                        target_module_partname = ''
-                        level = 1
-                        self.msg(2, 'SWIG import (caller=%r, fromlist=%r, level=%r)' % (source_module, target_attr_names, level))
-
-                        # Import this target SWIG C extension's package.
-                        try:
-                            target_modules = self.import_hook(
-                                target_module_partname, source_module,
-                                target_attr_names=None,
-                                level=level,
-                                edge_attr=edge_attr)
-                        except ImportError as msg:
-                            self.msg(2, "SWIG ImportError:", str(msg))
+                is_swig_import = is_swig_wrapper(source_module)
+                if is_swig_import:
+                    # Convert this Python 2-compliant implicit relative
+                    # import prohibited by Python 3 into a Python
+                    # 3-compliant explicit relative "from"-style import for
+                    # the duration of this function call by overwriting the
+                    # original parameters passed to this call.
+                    target_attr_names = [target_module_partname]
+                    target_module_partname = ''
+                    level = 1
+                    self.msg(2,
+                             'SWIG import (caller=%r, fromlist=%r, level=%r)'
+                             % (source_module, target_attr_names, level))
+                    # Import this target SWIG C extension's package.
+                    try:
+                        target_modules = self.import_hook(
+                            target_module_partname, source_module,
+                            target_attr_names=None,
+                            level=level,
+                            edge_attr=edge_attr)
+                    except ImportError as msg:
+                        self.msg(2, "SWIG ImportError:", str(msg))
 
             # If this module remains unimportable...
             if target_modules is None:
@@ -2303,21 +2304,31 @@ def is_swig_candidate():
         # Target module imported above.
         target_module = target_modules[0]
 
-        if isinstance(target_module, MissingModule) and is_swig_candidate():
+        if isinstance(target_module, MissingModule) \
+           and is_swig_import is None and is_swig_candidate() \
+           and is_swig_wrapper(source_module):
             # if this possible swig C module was previously imported from
             # a python module other than its corresponding swig python
             # module, then it may have been considered a MissingModule.
             # Try to reimport it now. For details see pull-request #2578
             # and issue #1522.
+            #
+            # If this module was takes as a SWIG candidate above, but failed
+            # to import, this would be a MissingModule, too. Thus check if
+            # this was the case (is_swig_import would be not None) to avoid
+            # recursion error. If `is_swig_import` is None and we are still a
+            # swig candidate then that means we haven't properly imported this
+            # swig module yet so do that below.
+            #
             # Remove the MissingModule node from the graph so that we can
             # attempt a reimport and avoid collisions. This node should be
             # fine to remove because the proper module will be imported and
             # added to the graph in the next line (call to _safe_import_hook).
             self.removeNode(target_module)
-            # Reimport the SWIG C module
+            # Reimport the SWIG C module relative to the wrapper
             target_modules = self._safe_import_hook(
                 target_module_partname, source_module,
-                target_attr_names=None, level=level, edge_attr=edge_attr)
+                target_attr_names=None, level=1, edge_attr=edge_attr)
             # return the output regardless because it would just be
             # duplicating the processing below
             return target_modules


Test patch:
diff --git a/tests/unit/test_modulegraph_more.py b/tests/unit/test_modulegraph_more.py
--- a/tests/unit/test_modulegraph_more.py
+++ b/tests/unit/test_modulegraph_more.py
@@ -88,6 +88,28 @@ def test_compiled_package(tmpdir):
     assert node.filename == str(pysrc) + 'c'
     assert node.packagepath == [str(pysrc.dirname)]
 
+
+#-- Basic tests - these seem to be missing in the original modulegraph
+#-- test-suite
+
+def test_relative_import_missing(tmpdir):
+    libdir = tmpdir.join('lib')
+    path = [str(libdir)]
+    pkg = libdir.join('pkg')
+    pkg.join('__init__.py').ensure().write('#')
+    pkg.join('x', '__init__.py').ensure().write('#')
+    pkg.join('x', 'y', '__init__.py').ensure().write('#')
+    pkg.join('x', 'y', 'z.py').ensure().write('from . import DoesNotExist')
+
+    script = tmpdir.join('script.py')
+    script.write('import pkg.x.y.z')
+    mg = modulegraph.ModuleGraph(path)
+    mg.run_script(str(script))
+    assert isinstance(mg.findNode('pkg.x.y.z'), modulegraph.SourceModule)
+    assert isinstance(mg.findNode('pkg.x.y.DoesNotExist'),
+                      modulegraph.MissingModule)
+
+
 #-- Tests with a single module in a zip-file
 
 def _zip_directory(filename, path):
@@ -353,3 +375,307 @@ def testfunc():
     assert module.is_global_attr('platform')
     assert not module.is_global_attr('shutil') # not imported at module level
     assert not module.is_global_attr('exitfunc')
+
+
+#-- SWIG packages - pyinstaller specific tests
+
+def test_swig_import_simple_BUGGY(tmpdir):
+    libdir = tmpdir.join('lib')
+    path = [str(libdir)]
+    osgeo = libdir.join('pyi_test_osgeo')
+    osgeo.join('__init__.py').ensure().write('#')
+    osgeo.join('pyi_gdal.py').write('# automatically generated by SWIG\n'
+                                    'import _pyi_gdal')
+    osgeo.join('_pyi_gdal.py').write('#')
+
+    script = tmpdir.join('script.py')
+    script.write('from pyi_test_osgeo import pyi_gdal')
+    mg = modulegraph.ModuleGraph(path)
+    mg.run_script(str(script))
+
+    assert isinstance(mg.findNode('pyi_test_osgeo'), modulegraph.Package)
+    assert isinstance(mg.findNode('pyi_test_osgeo.pyi_gdal'),
+                      modulegraph.SourceModule)
+    # The "C" module is frozen under its unqualified rather than qualified
+    # name. See comment in modulegraph._safe_import_hook.
+    # BUG: modulegraph contains a probable bug: Only the module's identifier
+    # is changed, not the module's graphident. Thus the node is still found
+    # under it's old name. The relevant code was brought from PyInstaller to
+    # upstream, so this might be PyInstaller's fault. See
+    # test_swig_import_simple for what it should be.
+    # This is a separate test-case, not marked as xfail, so we can spot
+    # whether the SWIG support works at all.
+    assert isinstance(mg.findNode('pyi_test_osgeo._pyi_gdal'),
+                      modulegraph.SourceModule)
+    if is_py2:
+        # In Python 2.7 the relative import works as expected.
+        assert mg.findNode('pyi_test_osgeo._pyi_gdal').identifier \
+            == 'pyi_test_osgeo._pyi_gdal'
+        assert mg.findNode('_pyi_gdal') is None
+    else:
+        # Due the the buggy implementation, the graphident is unchanged, but
+        # at least the identifier should have changed.
+        assert mg.findNode('pyi_test_osgeo._pyi_gdal').identifier \
+            == '_pyi_gdal'
+        # Due the the buggy implementation, this node does not exist.
+        assert mg.findNode('_pyi_gdal') is None
+    return mg  # for use in test_swig_import_simple_BUG
+
+
+@xfail
+def test_swig_import_simple(tmpdir):
+    # Test the expected (but not implemented) behavior if SWIG support.
+    mg = test_swig_import_simple_BUGGY(tmpdir)
+    # Given the bug in modulegraph (see test_swig_import_simple_BUGGY) this is
+    # what would be the expected behavior.
+    # TODO: When modulegraph is fixed, merge the two test-cases and correct
+    # test_swig_import_from_top_level and siblings.
+    assert mg.findNode('pyi_test_osgeo._pyi_gdal') is None
+    assert isinstance(mg.findNode('_pyi_gdal'), modulegraph.SourceModule)
+
+
+def test_swig_import_from_top_level(tmpdir):
+    # While there is a SWIG wrapper module as expected, the package module
+    # already imports the "C" module in the same way the SWIG wrapper would
+    # do.
+    # See the issue #1522 (at about 2017-04-26), pull-request #2578 and commit
+    # 711e9e77c93a979a63648ba05f725b30dbb7c3cc.
+    #
+    # For Python > 2.6, SWIG tries to import the C module from the package's
+    # directory and if this fails, uses "import _XXX" (which is the code
+    # triggering import in modulegraph). For Python 2 this is a relative
+    # import, but for Python 3 this is an absolute import.
+    #
+    # In this test-case, the package's __init__.py contains code equivalent to
+    # the SWIG wrapper-module, causing the C module to be searched as an
+    # absolute import (in Python 3). But the importing module is not a SWIG
+    # candidate (names do not match), leading to the (absolute) C module to
+    # become a MissingModule - which is okay up to this point. Now if the SWIG
+    # wrapper-module imports the C module, there already is this
+    # MissingModule, inhibiting modulegraph's SWIG import mechanism.
+    #
+    # This is where commit 711e9e77c93 steps in and tries to reimport the C
+    # module (relative to the SWIG wrapper-module).
+    libdir = tmpdir.join('lib')
+    path = [str(libdir)]
+    osgeo = libdir.join('pyi_test_osgeo')
+    osgeo.join('__init__.py').ensure().write('import _pyi_gdal')
+    osgeo.join('pyi_gdal.py').write('# automatically generated by SWIG\n'
+                                    'import _pyi_gdal')
+    osgeo.join('_pyi_gdal.py').write('#')
+
+    script = tmpdir.join('script.py')
+    script.write('from pyi_test_osgeo import pyi_gdal')
+    mg = modulegraph.ModuleGraph(path)
+    mg.run_script(str(script))
+
+    assert isinstance(mg.findNode('pyi_test_osgeo'), modulegraph.Package)
+    assert isinstance(mg.findNode('pyi_test_osgeo.pyi_gdal'),
+                      modulegraph.SourceModule)
+    # The "C" module is frozen under its unqualified rather than qualified
+    # name. See comment in modulegraph._safe_import_hook.
+    # Due the the buggy implementation (see test_swig_import_simple):
+    assert isinstance(mg.findNode('pyi_test_osgeo._pyi_gdal'),
+                      modulegraph.SourceModule)
+    assert mg.findNode('_pyi_gdal') is None
+    # This would be the correct implementation:
+    #assert mg.findNode('pyi_test_osgeo._pyi_gdal') is None
+    #assert isinstance(mg.findNode('_pyi_gdal'), modulegraph.SourceModule)
+
+
+def test_swig_import_from_top_level_missing(tmpdir):
+    # Like test_swig_import_from_top_level, but the "C" module is missing and
+    # should be reported as a MissingModule.
+    libdir = tmpdir.join('lib')
+    path = [str(libdir)]
+    osgeo = libdir.join('pyi_test_osgeo')
+    osgeo.join('__init__.py').ensure().write('import _pyi_gdal')
+    osgeo.join('pyi_gdal.py').write('# automatically generated by SWIG\n'
+                                    'import _pyi_gdal')
+    # no module '_pyi_gdal.py'
+
+    script = tmpdir.join('script.py')
+    script.write('from pyi_test_osgeo import pyi_gdal')
+    mg = modulegraph.ModuleGraph(path)
+    mg.run_script(str(script))
+    assert isinstance(mg.findNode('pyi_test_osgeo'),
+                      modulegraph.Package)
+    assert isinstance(mg.findNode('pyi_test_osgeo.pyi_gdal'),
+                      modulegraph.SourceModule)
+    # BUG: Again, this is unecpected behaviour in modulegraph: While
+    # MissingModule('_pyi_gdal') is (arguable) removed when trying to import
+    # the SWIG C module, there is no MissingModule('pyi_test_osgeo.pyi_gdal')
+    # added, but again MissingModule('_pyi_gdal'). I still need to understand
+    # why.
+    assert mg.findNode('pyi_test_osgeo._pyi_gdal') is None
+    assert isinstance(mg.findNode('_pyi_gdal'), modulegraph.MissingModule)
+
+
+def test_swig_import_from_top_level_but_nested(tmpdir):
+    # Like test_swig_import_from_top_level, but both the wrapper and the "top
+    # level" are nested. This is intented to test relative import of the "C"
+    # module.
+    libdir = tmpdir.join('lib')
+    path = [str(libdir)]
+    osgeo = libdir.join('pyi_test_osgeo')
+    osgeo.join('__init__.py').ensure().write('#')
+    osgeo.join('x', '__init__.py').ensure().write('#')
+    osgeo.join('x', 'y', '__init__.py').ensure().write('import _pyi_gdal')
+    osgeo.join('x', 'y', 'pyi_gdal.py').write(
+        '# automatically generated by SWIG\n'
+        'import _pyi_gdal')
+    osgeo.join('x', 'y', '_pyi_gdal.py').write('#')
+
+    script = tmpdir.join('script.py')
+    script.write('from pyi_test_osgeo.x.y import pyi_gdal')
+    mg = modulegraph.ModuleGraph(path)
+    mg.run_script(str(script))
+
+    assert isinstance(mg.findNode('pyi_test_osgeo.x.y.pyi_gdal'),
+                      modulegraph.SourceModule)
+    # The "C" module is frozen under its unqualified rather than qualified
+    # name. See comment in modulegraph._safe_import_hook.
+    # Due the the buggy implementation (see test_swig_import_simple):
+    assert isinstance(mg.findNode('pyi_test_osgeo.x.y._pyi_gdal'),
+                      modulegraph.SourceModule)
+    assert mg.findNode('_pyi_gdal') is None
+    # This would be the correct implementation:
+    #assert mg.findNode('pyi_test_osgeo.x.y._pyi_gdal') is None
+    #assert isinstance(mg.findNode('_pyi_gdal'), modulegraph.SourceModule)
+
+
+def test_swig_top_level_but_no_swig_at_all(tmpdir):
+    # From the script import an absolute module which looks like a SWIG
+    # candidate but is no SWIG module. See issue #3040 ('_decimal')
+    # The center of this test-case is that it doesn't raise a recursion too
+    # deep error.
+    libdir = tmpdir.join('lib')
+    path = [str(libdir)]
+    libdir.join('pyi_dezimal.py').ensure().write('import _pyi_dezimal')
+    # no module '_pyi_dezimal.py'
+
+    script = tmpdir.join('script.py')
+    script.write('import pyi_dezimal')
+    mg = modulegraph.ModuleGraph(path)
+    mg.run_script(str(script))
+    assert isinstance(mg.findNode('pyi_dezimal'), modulegraph.SourceModule)
+    assert isinstance(mg.findNode('_pyi_dezimal'), modulegraph.MissingModule)
+
+
+def test_swig_top_level_but_no_swig_at_all_existing(tmpdir):
+    # Like test_swig_top_level_but_no_swig_at_all, but the "C" module exists.
+    # The test-case is here for symmetry.
+    libdir = tmpdir.join('lib')
+    path = [str(libdir)]
+    libdir.join('pyi_dezimal.py').ensure().write('import _pyi_dezimal')
+    libdir.join('_pyi_dezimal.py').ensure().write('#')
+
+    script = tmpdir.join('script.py')
+    script.write('import pyi_dezimal')
+    mg = modulegraph.ModuleGraph(path)
+    mg.run_script(str(script))
+    assert isinstance(mg.findNode('pyi_dezimal'), modulegraph.SourceModule)
+    assert isinstance(mg.findNode('_pyi_dezimal'), modulegraph.SourceModule)
+
+
+def test_swig_candidate_but_not_swig(tmpdir):
+    # From a package module import an absolute module which looks like a SWIG
+    # candidate but is no SWIG module . See issue #2911 (tifffile).
+    # The center of this test-case is that it doesn't raise a recursion too
+    # deep error.
+    libdir = tmpdir.join('lib')
+    path = [str(libdir)]
+    pkg = libdir.join('pkg')
+    pkg.join('__init__.py').ensure().write('from . import mymod')
+    pkg.join('mymod.py').write('import _mymod')
+    pkg.join('_mymod.py').write('#')
+
+    script = tmpdir.join('script.py')
+    script.write('from pkg import XXX')
+    mg = modulegraph.ModuleGraph(path)
+    mg.run_script(str(script))
+    assert isinstance(mg.findNode('pkg'), modulegraph.Package)
+    assert isinstance(mg.findNode('pkg.mymod'), modulegraph.SourceModule)
+    if is_py2:
+        # In Python 2 this is a relative import, global module should exist
+        assert isinstance(mg.findNode('pkg._mymod'), modulegraph.SourceModule)
+        assert mg.findNode('_mymod') is None
+    else:
+        assert mg.findNode('pkg._mymod') is None
+        # This is not a SWIG module, thus the SWIG import mechanism should not
+        # trigger.
+        assert isinstance(mg.findNode('_mymod'), modulegraph.MissingModule)
+
+
+def test_swig_candidate_but_not_swig2(tmpdir):
+    """
+    Variation of test_swig_candidate_but_not_swig using differnt import
+    statements (like tifffile/tifffile.py does)
+    """
+    libdir = tmpdir.join('lib')
+    path = [str(libdir)]
+    pkg = libdir.join('pkg')
+    pkg.join('__init__.py').ensure().write('from . import mymod')
+    pkg.join('mymod.py').write('from . import _mymod\n'
+                               'import _mymod')
+    pkg.join('_mymod.py').write('#')
+
+    script = tmpdir.join('script.py')
+    script.write('from pkg import XXX')
+    mg = modulegraph.ModuleGraph(path)
+    mg.run_script(str(script))
+    assert isinstance(mg.findNode('pkg'), modulegraph.Package)
+    assert isinstance(mg.findNode('pkg.mymod'), modulegraph.SourceModule)
+    assert isinstance(mg.findNode('pkg._mymod'), modulegraph.SourceModule)
+    if is_py2:
+        # In Python 2 both are relative imports, global module should not exist
+        assert mg.findNode('_mymod') is None
+    else:
+        assert isinstance(mg.findNode('_mymod'), modulegraph.MissingModule)
+
+
+def test_swig_candidate_but_not_swig_missing(tmpdir):
+    # Like test_swig_candidate_but_not_swig, but the "C" module is missing and
+    # should be reported as a MissingModule.
+    libdir = tmpdir.join('lib')
+    path = [str(libdir)]
+    pkg = libdir.join('pkg')
+    pkg.join('__init__.py').ensure().write('from . import mymod')
+    pkg.join('mymod.py').write('import _mymod')
+    # no module '_mymod.py'
+
+    script = tmpdir.join('script.py')
+    script.write('import pkg')
+    mg = modulegraph.ModuleGraph(path)
+    mg.run_script(str(script))
+    assert isinstance(mg.findNode('pkg'), modulegraph.Package)
+    assert isinstance(mg.findNode('pkg.mymod'), modulegraph.SourceModule)
+    assert mg.findNode('pkg._mymod') is None
+    assert isinstance(mg.findNode('_mymod'), modulegraph.MissingModule)
+
+
+def test_swig_candidate_but_not_swig_missing2(tmpdir):
+    """
+    Variation of test_swig_candidate_but_not_swig_missing using differnt import
+    statements (like tifffile/tifffile.py does)
+    """
+    libdir = tmpdir.join('lib')
+    path = [str(libdir)]
+    pkg = libdir.join('pkg')
+    pkg.join('__init__.py').ensure().write('from . import mymod')
+    pkg.join('mymod.py').write('from . import _mymod\n'
+                               'import _mymod')
+    # no module '_mymod.py'
+
+    script = tmpdir.join('script.py')
+    script.write('import pkg')
+    mg = modulegraph.ModuleGraph(path)
+    mg.run_script(str(script))
+    assert isinstance(mg.findNode('pkg'), modulegraph.Package)
+    assert isinstance(mg.findNode('pkg.mymod'), modulegraph.SourceModule)
+    assert isinstance(mg.findNode('pkg._mymod'), modulegraph.MissingModule)
+    if is_py2:
+        # In Python 2 both are relative imports, global module should not exist
+        assert mg.findNode('_mymod') is None
+    else:
+        assert isinstance(mg.findNode('_mymod'), modulegraph.MissingModule)