Repository: django-oscar/django-oscar

Issue:
Don't fallback to default apps when ImportError is raised
Thanks to dynamically loading apps, there is no need to override Oscar application and list your app. (But you can still do it if you want) Dynamically loading app fallbacks to standard Oscar application when ImportError is raised.

Be clever about ImportError and fallback only when imported app can't fe found (not when a dependency of that app can't be found)



Discussion:

Resolving patch:
diff --git a/oscar/core/loading.py b/oscar/core/loading.py
--- a/oscar/core/loading.py
+++ b/oscar/core/loading.py
@@ -1,3 +1,5 @@
+import sys
+import traceback
 from imp import new_module
 
 from django.conf import settings
@@ -52,6 +54,23 @@ class can't be found in the overriding module, then we attempt to import it
     try:
         imported_local_module = __import__(local_app, fromlist=classnames)
     except ImportError:
+        # There are 2 reasons why there is ImportError:
+        #  1. local_app does not exist
+        #  2. local_app exists but is corrupted (ImportError inside of the app)
+        #
+        # Obviously, for the reason #1 we want to fail back to use Oscar app.
+        # For the reason #2 we want to propagate error (the dev obviously wants
+        # to override app and not use Oscar app)
+        #
+        # ImportError does not provide easy way to distinguish those two cases.
+        # Fortunatelly, the traceback of the ImportError starts at __import__
+        # statement. If the traceback has more than one frame, it means that
+        # application was found and ImportError originates within the local app
+        __, __, exc_traceback = sys.exc_info()
+        frames = traceback.extract_tb(exc_traceback)
+        if len(frames) > 1:
+            raise
+
         # Module not in local app
         imported_local_module = {}
     oscar_app = "oscar.apps.%s" % module_label


Test patch:
diff --git a/tests/_site/import_error_app/__init__.py b/tests/_site/import_error_app/__init__.py
new file mode 100644
diff --git a/tests/_site/import_error_app/catalogue/__init__.py b/tests/_site/import_error_app/catalogue/__init__.py
new file mode 100644
diff --git a/tests/_site/import_error_app/catalogue/app.py b/tests/_site/import_error_app/catalogue/app.py
new file mode 100644
--- /dev/null
+++ b/tests/_site/import_error_app/catalogue/app.py
@@ -0,0 +1,6 @@
+# On purpose raise ImportError
+from django import NonExistingApp
+
+
+class CatalogueApplication(NonExistingApp):
+    pass
diff --git a/tests/_site/import_error_app/catalogue/models.py b/tests/_site/import_error_app/catalogue/models.py
new file mode 100644
diff --git a/tests/unit/core/loading_tests.py b/tests/unit/core/loading_tests.py
--- a/tests/unit/core/loading_tests.py
+++ b/tests/unit/core/loading_tests.py
@@ -49,6 +49,13 @@ def test_raise_exception_when_bad_classname_used(self):
         with self.assertRaises(ClassNotFoundError):
             get_class('catalogue.models', 'Monkey')
 
+    def test_raise_importerror_if_app_raises_importerror(self):
+        installed_apps = list(settings.INSTALLED_APPS)
+        installed_apps.insert(0, 'tests._site.import_error_app.catalogue')
+        with override_settings(INSTALLED_APPS=installed_apps):
+            with self.assertRaises(ImportError):
+                get_class('catalogue.app', 'CatalogueApplication')
+
 
 class ClassLoadingWithLocalOverrideTests(TestCase):
 
@@ -119,4 +126,3 @@ def test_uses_dashboard_override_when_specified(self):
         self.assertTrue('apps.dashboard.catalogue' in apps)
         self.assertTrue('oscar.apps.dashboard.catalogue' not in apps)
         self.assertTrue('oscar.apps.catalogue' in apps)
-