Repository: django-extensions/django-extensions

Issue:
More sensible error message for runscript
I wasted a lot of time discovering that runscript gives the same error message for when there is an import error in the script and when it cannot find the script at all. Please add a check for whether the file exists at all first, and throw a distinct error if it cannot find any such script.

[StackOverflow example.](http://stackoverflow.com/questions/39322967/djangos-runscript-no-valid-module-for-script-filename-found)



Discussion:
Hey @Deleetdk, I have just been looking through this and I would like to tackle it. Sorry for the bad experience you had to go through to discover this bug.

After looking into it, I think the error message for a non-existent script is correct. 
i.e. `No (valid) module for script 'test_script' found`

I think the error message for when there's an import error in the script is what needs to be fixed.
e.g. In the case where `import nonexistrentpackage` is at the top of the file.

However, you will notice that when the invalid import is placed inside the `run` function, the correct error message is raised. For example, see the following code and the error message it raises.

``` python
def run():
    import nonexistent
    print("Test")
```

Exception:

```
...
ImportError: No module named nonexistent
```

I think this is what should be raised when there is an invalid import. 
What do you think? 

Makes sense. However, I didn't know that I had to put my imports inside `run()` too. I noticed that [the official docs](http://django-extensions.readthedocs.io/en/latest/runscript.html) also have imports outside `run()`. It's not a common coding pattern to do, so it's likely that many others will run into this same problem.

@Deleetdk that's not the recommended way to do it. I just tried it out and noticed it handled the error correctly.
I'll try to see whether I can replicate that kind of error even when the invalid import is at the top level.


Resolving patch:
diff --git a/django_extensions/management/commands/runscript.py b/django_extensions/management/commands/runscript.py
--- a/django_extensions/management/commands/runscript.py
+++ b/django_extensions/management/commands/runscript.py
@@ -4,6 +4,7 @@
 import traceback
 
 from django.apps import apps
+from django.core.management.base import CommandError
 
 from django_extensions.management.email_notifications import EmailNotificationCommand
 from django_extensions.management.utils import signalcommand
@@ -77,7 +78,7 @@ def run_script(mod, *script_args):
                 mod.run(*script_args)
                 if email_notifications:
                     self.send_email_notification(notification_id=mod.__name__)
-            except Exception:
+            except Exception as e:
                 if silent:
                     return
                 if verbosity > 0:
@@ -85,7 +86,7 @@ def run_script(mod, *script_args):
                 if email_notifications:
                     self.send_email_notification(
                         notification_id=mod.__name__, include_traceback=True)
-                if show_traceback:
+                if show_traceback or not isinstance(e, CommandError):
                     raise
 
         def my_import(mod):
@@ -93,9 +94,8 @@ def my_import(mod):
                 print(NOTICE("Check for %s" % mod))
             # check if module exists before importing
             try:
-                importlib.import_module(mod)
-                t = __import__(mod, [], [], [" "])
-            except (ImportError, AttributeError) as e:
+                t = importlib.import_module(mod)
+            except ImportError as e:
                 if str(e).startswith('No module named'):
                     try:
                         exc_type, exc_value, exc_traceback = sys.exc_info()
@@ -107,9 +107,8 @@ def my_import(mod):
                     finally:
                         exc_traceback = None
 
-                if verbosity > 1:
-                    if verbosity > 2:
-                        traceback.print_exc()
+                if not silent:
+                    traceback.print_exc()
                     print(ERROR("Cannot import module '%s': %s." % (mod, e)))
 
                 return False
@@ -120,7 +119,7 @@ def my_import(mod):
                 return t
             else:
                 if verbosity > 1:
-                    print(ERROR2("Find script '%s' but no run() function found." % mod))
+                    print(ERROR2("Found script '%s' but no run() function found." % mod))
 
         def find_modules_for_script(script):
             """ find script module which contains 'run' attribute """


Test patch:
diff --git a/tests/test_runscript.py b/tests/test_runscript.py
--- a/tests/test_runscript.py
+++ b/tests/test_runscript.py
@@ -25,3 +25,17 @@ def test_runs_appconfig(self):
             call_command('runscript', 'sample_script', verbosity=2)
             self.assertIn("Found script 'tests.testapp.scripts.sample_script'", sys.stdout.getvalue())
             self.assertIn("Running script 'tests.testapp.scripts.sample_script'", sys.stdout.getvalue())
+
+    def test_prints_error_on_nonexistent_script(self):
+        call_command('runscript', 'non_existent_script', verbosity=2)
+        self.assertIn("No (valid) module for script 'non_existent_script' found", sys.stdout.getvalue())
+
+    def test_prints_additional_info_on_nonexistent_script_and_low_verbosity(self):
+        call_command('runscript', 'non_existent_script', verbosity=1)
+        self.assertIn("No (valid) module for script 'non_existent_script' found", sys.stdout.getvalue())
+        self.assertIn("Try running with a higher verbosity level like: -v2 or -v3", sys.stdout.getvalue())
+
+    def test_prints_import_error_on_script_with_invalid_imports(self):
+        call_command('runscript', 'invalid_import_script', verbosity=2)
+        self.assertIn("Cannot import module 'tests.testapp.scripts.invalid_import_script'", sys.stdout.getvalue())
+        self.assertRegexpMatches(sys.stdout.getvalue(), 'No module named (\')?(invalidpackage)\1?')
diff --git a/tests/testapp/scripts/invalid_import_script.py b/tests/testapp/scripts/invalid_import_script.py
new file mode 100644
--- /dev/null
+++ b/tests/testapp/scripts/invalid_import_script.py
@@ -0,0 +1,6 @@
+# -*- coding: utf-8 -*-
+import invalidpackage # NOQA
+
+
+def run():
+    pass