Repository: podhmo/magicalimport

Issue:
raise AttributeError when target file is not found, instead of AttributeError
```
>>> from magicalimport import import_from_physical_path
>>> import_from_physical_path("jifoewjifoew.py")
...
FileNotFoundError: [Errno 2] No such file or directory: 'jifoewjifoew.py'
>>> import_from_physical_path("jifoewjifoew.p")
...
AttributeError: 'NoneType' object has no attribute 'loader'
```


Discussion:

Patch:
diff --git a/magicalimport/__init__.py b/magicalimport/__init__.py
index 48a5ff0..6c50b0f 100644
--- a/magicalimport/__init__.py
+++ b/magicalimport/__init__.py
@@ -25,7 +25,10 @@ def import_from_physical_path(path, as_=None, here=None):
     module_id = as_ or path.replace("/", "_").rsplit(".py", 1)[0]
     if module_id in sys.modules:
         return sys.modules[module_id]
-    return _create_module(module_id, path)
+    try:
+        return _create_module(module_id, path)
+    except FileNotFoundError as e:
+        raise ModuleNotFoundError(e) from None
 
 
 def import_module(module_path, here=None, sep=":"):
@@ -36,13 +39,19 @@ def import_module(module_path, here=None, sep=":"):
         return import_module_original(module_path)
 
 
-def import_symbol(sym, here=None, sep=":", ns=None):
+def import_symbol(sym, here=None, sep=":", ns=None, silent=False):
     if ns is not None and sep not in sym:
         sym = "{}{}{}".format(ns, sep, sym)
     module_path, fn_name = sym.rsplit(sep, 2)
     try:
         module = import_module(module_path, here=here, sep=sep)
-        return getattr(module, fn_name)
-    except (ImportError, AttributeError) as e:
-        sys.stderr.write("could not import {!r}\n{}\n".format(sym, e))
+    except ImportError as e:  # ModuleNotFoundError is subclass of ImportError
+        if not silent:
+            sys.stderr.write("could not import {!r}\n{}\n".format(sym, e))
         raise
+    try:
+        return getattr(module, fn_name)
+    except AttributeError as e:
+        if not silent:
+            sys.stderr.write("could not import {!r}\n{}\n".format(sym, e))
+        raise ImportError(e) from None


Test patch:
diff --git a/magicalimport/tests/test_notfound.py b/magicalimport/tests/test_notfound.py
new file mode 100644
index 0000000..3a38fb3
--- /dev/null
+++ b/magicalimport/tests/test_notfound.py
@@ -0,0 +1,60 @@
+import unittest
+import os.path
+
+here = os.path.dirname(os.path.abspath(__file__))
+
+
+class ImportModuleTests(unittest.TestCase):
+    def _callFUT(self, *args, **kwargs):
+        from magicalimport import import_module
+
+        return import_module(*args, **kwargs)
+
+    def test_target_file_is_notfound(self):
+        import random
+
+        path = os.path.join(here, "missing{}.py".format(random.random()))
+        with self.assertRaises(ModuleNotFoundError):
+            self._callFUT(path)
+
+    def test_target_module_is_notfound(self):
+        import random
+
+        path = os.path.join(here, "missing{}".format(random.random()))
+        with self.assertRaises(ModuleNotFoundError):
+            self._callFUT(path)
+
+
+class ImportSymbolTests(unittest.TestCase):
+    def _callFUT(self, *args, **kwargs):
+        from magicalimport import import_symbol
+
+        kwargs["silent"] = True
+        return import_symbol(*args, **kwargs)
+
+    def test_target_file_is_notfound(self):
+        import random
+
+        path = os.path.join(here, "missing{}.py:foo".format(random.random()))
+        with self.assertRaises(ModuleNotFoundError):
+            self._callFUT(path)
+
+    def test_target_module_is_notfound(self):
+        import random
+
+        path = os.path.join(here, "missing{}:foo".format(random.random()))
+        with self.assertRaises(ModuleNotFoundError):
+            self._callFUT(path)
+
+    def test_target_module_has_not_member(self):
+        with self.assertRaises(ImportError) as c:
+            self._callFUT("collections:foo")
+
+    def test_target_module_has_not_member(self):
+        import os.path
+
+        path = os.path.join(here, "../../examples/a/b/c/foo.py")
+        self.assertTrue(os.path.exists(path))
+
+        with self.assertRaises(ImportError):
+            self._callFUT("{}:foo".format(path))