Repository: conan-io/conan

Issue:
[bug] conanfile loader "crashes" because of missing __file__ attribute in Python 3.7(+)
Hi there,
I am using multiple Python versions (e.g. 3.6, 3.7). My primary Python version is 3.6.
But occationally I execute Python version 3.7. With 3.6 there where no problems. 3.7 does not work.

I have 2 projects. One (```pkg_a```) gets ```python_requires```d into the other (```pkg_b```).
pkg_b inherits from a python file that gets exported with pkg_a. Now Python 3.6 everything works fine. In Python 3.7 I get an exception.

Crawling through the conan code I identified (at least I think) the code where the solution lies.
At [this code line](https://github.com/conan-io/conan/blob/4fdebfb79ce12f956b4be512253e8479e9d6bb17/conans/client/loader.py#L265) conan can catch an exception if the python module has no ```__file__``` attribute.
But in Python 3.7 I get a ```TypeError``` exception instead of an ```AttributeError``` exception.

The [Python docs describe](https://docs.python.org/3.6/library/exceptions.html#AttributeError) ([see](https://docs.python.org/3.6/library/exceptions.html#AttributeError)) that there could be raised a ```TypeError``` exception.

In turn I modified the code line [265](https://github.com/conan-io/conan/blob/4fdebfb79ce12f956b4be512253e8479e9d6bb17/conans/client/loader.py#L265) in the conan [loader.py](https://github.com/conan-io/conan/blob/develop/conans/client/loader.py).

From:
```python
except AttributeError: # some module doesn't have __file__
    pass
```

To:
```python
except AttributeError: # some module doesn't have __file__
    pass
except TypeError: # Python 3.7: some module doesn't have __file__
    pass
```

And everything worked like before.
I do not know if this is a Python or Conan problem. But at the moment this is a real issue as noone with a version of Python 3.7+ can execute my recipe.

For reference I made a minimal example to track down the issue.
[py36_py37_exception.zip](https://github.com/conan-io/conan/files/2915266/py36_py37_exception.zip)


Discussion:

Patch:
diff --git a/conans/client/hook_manager.py b/conans/client/hook_manager.py
index be634d2ee..25f7fd8a0 100644
--- a/conans/client/hook_manager.py
+++ b/conans/client/hook_manager.py
@@ -101,8 +101,14 @@ class HookManager(object):
                 module = sys.modules[added]
                 if module:
                     try:
-                        folder = os.path.dirname(module.__file__)
-                    except AttributeError:  # some module doesn't have __file__
+                        try:
+                            # Most modules will have __file__ != None
+                            folder = os.path.dirname(module.__file__)
+                        except (AttributeError, TypeError):
+                            # But __file__ might not exist or equal None
+                            # Like some builtins and Namespace packages py3
+                            folder = module.__path__._path[0]
+                    except AttributeError:  # In case the module.__path__ doesn't exist
                         pass
                     else:
                         if folder.startswith(current_dir):
diff --git a/conans/client/loader.py b/conans/client/loader.py
index 9bd73cab0..361e0628a 100644
--- a/conans/client/loader.py
+++ b/conans/client/loader.py
@@ -261,8 +261,14 @@ def _parse_conanfile(conan_file_path):
             module = sys.modules[added]
             if module:
                 try:
-                    folder = os.path.dirname(module.__file__)
-                except AttributeError:  # some module doesn't have __file__
+                    try:
+                        # Most modules will have __file__ != None
+                        folder = os.path.dirname(module.__file__)
+                    except (AttributeError, TypeError):
+                        # But __file__ might not exist or equal None
+                        # Like some builtins and Namespace packages py3
+                        folder = module.__path__._path[0]
+                except AttributeError:  # In case the module.__path__ doesn't exist
                     pass
                 else:
                     if folder.startswith(current_dir):


Test patch:
diff --git a/conans/test/unittests/client/conanfile_loader_test.py b/conans/test/unittests/client/conanfile_loader_test.py
index 9ad90f4ba..898aaf622 100644
--- a/conans/test/unittests/client/conanfile_loader_test.py
+++ b/conans/test/unittests/client/conanfile_loader_test.py
@@ -1,21 +1,27 @@
 import os
+import sys
+import textwrap
 import unittest
 from collections import OrderedDict
 
+import six
 from mock import Mock
 from mock.mock import call
+from parameterized import parameterized
 
 from conans.client.graph.python_requires import ConanPythonRequire
-from conans.client.loader import ConanFileLoader, ConanFileTextLoader
+from conans.client.loader import ConanFileLoader, ConanFileTextLoader,\
+    _parse_conanfile
+from conans.client.tools.files import chdir
 from conans.errors import ConanException
 from conans.model.options import OptionsValues
 from conans.model.profile import Profile
 from conans.model.requires import Requirements
 from conans.model.settings import Settings
 from conans.test.utils.test_files import temp_folder
-from conans.util.files import save
 from conans.test.utils.tools import test_processed_profile,\
     TestBufferConanOutput
+from conans.util.files import save
 
 
 class ConanLoaderTest(unittest.TestCase):
@@ -225,3 +231,102 @@ class MyTest(ConanFile):
         recipe = loader.load_consumer(conanfile_path,
                                       test_processed_profile(profile))
         self.assertIsNone(recipe.settings.os.value)
+
+
+class ImportModuleLoaderTest(unittest.TestCase):
+    @staticmethod
+    def _create_and_load(myfunc, value, subdir_name, add_subdir_init):
+        subdir_content = textwrap.dedent("""
+            def get_value():
+                return {value}
+            def {myfunc}():
+                return "{myfunc}"
+        """)
+
+        side_content = textwrap.dedent("""
+            def get_side_value():
+                return {value}
+            def side_{myfunc}():
+                return "{myfunc}"
+        """)
+
+        conanfile = textwrap.dedent("""
+            import pickle
+            from {subdir}.api import get_value, {myfunc}
+            from file import get_side_value, side_{myfunc}
+            from fractions import Fraction
+            def conanfile_func():
+                return get_value(), {myfunc}(), get_side_value(), side_{myfunc}(), str(Fraction(1,1))
+        """)
+        expected_return = (value, myfunc, value, myfunc, "1")
+
+        tmp = temp_folder()
+        with chdir(tmp):
+            save("conanfile.py", conanfile.format(value=value, myfunc=myfunc, subdir=subdir_name))
+            save("file.py", side_content.format(value=value, myfunc=myfunc))
+            save("{}/api.py".format(subdir_name), subdir_content.format(value=value, myfunc=myfunc))
+            if add_subdir_init:
+                save("__init__.py", "")
+                save("{}/__init__.py".format(subdir_name), "")
+
+        loaded, module_id = _parse_conanfile(os.path.join(tmp, "conanfile.py"))
+        return loaded, module_id, expected_return
+
+    @parameterized.expand([(True, False), (False, True), (False, False)])
+    @unittest.skipIf(six.PY2, "Python 2 requires __init__.py file in modules")
+    def test_py3_recipe_colliding_init_filenames(self, sub1, sub2):
+        myfunc1, value1 = "recipe1", 42
+        myfunc2, value2 = "recipe2", 23
+        loaded1, module_id1, exp_ret1 = self._create_and_load(myfunc1, value1, "subdir", sub1)
+        loaded2, module_id2, exp_ret2 = self._create_and_load(myfunc2, value2, "subdir", sub2)
+
+        self.assertNotEqual(module_id1, module_id2)
+        self.assertEqual(loaded1.conanfile_func(), exp_ret1)
+        self.assertEqual(loaded2.conanfile_func(), exp_ret2)
+
+    def test_recipe_colliding_filenames(self):
+        myfunc1, value1 = "recipe1", 42
+        myfunc2, value2 = "recipe2", 23
+        loaded1, module_id1, exp_ret1 = self._create_and_load(myfunc1, value1, "subdir", True)
+        loaded2, module_id2, exp_ret2 = self._create_and_load(myfunc2, value2, "subdir", True)
+
+        self.assertNotEqual(module_id1, module_id2)
+        self.assertEqual(loaded1.conanfile_func(), exp_ret1)
+        self.assertEqual(loaded2.conanfile_func(), exp_ret2)
+
+    @parameterized.expand([(True, ), (False, )])
+    def test_wrong_imports(self, add_subdir_init):
+        myfunc1, value1 = "recipe1", 42
+
+        # Item imported does not exist, but file exists
+        with self.assertRaisesRegexp(ConanException, "Unable to load conanfile in"):
+            self._create_and_load(myfunc1, value1, "requests", add_subdir_init)
+
+        # File does not exists in already existing module
+        with self.assertRaisesRegexp(ConanException, "Unable to load conanfile in"):
+            self._create_and_load(myfunc1, value1, "conans", add_subdir_init)
+
+    def test_helpers_python_library(self):
+        mylogger = """
+value = ""
+def append(data):
+    global value
+    value += data
+"""
+        temp = temp_folder()
+        save(os.path.join(temp, "myconanlogger.py"), mylogger)
+
+        conanfile = "import myconanlogger"
+        temp1 = temp_folder()
+        save(os.path.join(temp1, "conanfile.py"), conanfile)
+        temp2 = temp_folder()
+        save(os.path.join(temp2, "conanfile.py"), conanfile)
+
+        try:
+            sys.path.append(temp)
+            loaded1, _ = _parse_conanfile(os.path.join(temp1, "conanfile.py"))
+            loaded2, _ = _parse_conanfile(os.path.join(temp2, "conanfile.py"))
+            self.assertIs(loaded1.myconanlogger, loaded2.myconanlogger)
+            self.assertIs(loaded1.myconanlogger.value, loaded2.myconanlogger.value)
+        finally:
+            sys.path.remove(temp)