diff --git a/django/core/management/commands/shell.py b/django/core/management/commands/shell.py index eb3cf04e81..89d12ce6b8 100644 --- a/django/core/management/commands/shell.py +++ b/django/core/management/commands/shell.py @@ -6,6 +6,7 @@ from collections import defaultdict from importlib import import_module from django.apps import apps +from django.core.exceptions import AppRegistryNotReady from django.core.management import BaseCommand, CommandError from django.utils.datastructures import OrderedSet from django.utils.module_loading import import_string as import_dotted_path @@ -150,6 +151,22 @@ class Command(BaseCommand): if options and options.get("no_imports"): return {} + verbosity = options["verbosity"] if options else 0 + + try: + apps.check_models_ready() + except AppRegistryNotReady: + if verbosity > 0: + settings_env_var = os.getenv("DJANGO_SETTINGS_MODULE") + self.stdout.write( + "Automatic imports are disabled since settings are not configured." + f"\nDJANGO_SETTINGS_MODULE value is {settings_env_var!r}.\n" + "HINT: Ensure that the settings module is configured and set.", + self.style.ERROR, + ending="\n\n", + ) + return {} + path_imports = self.get_auto_imports() if path_imports is None: return {} @@ -175,7 +192,6 @@ class Command(BaseCommand): name: obj for items in auto_imports.values() for name, obj in items } - verbosity = options["verbosity"] if options else 0 if verbosity < 1: return namespace @@ -228,7 +244,7 @@ class Command(BaseCommand): def handle(self, **options): # Execute the command and exit. if options["command"]: - exec(options["command"], {**globals(), **self.get_namespace()}) + exec(options["command"], {**globals(), **self.get_namespace(**options)}) return # Execute stdin if it has anything to read and exit. @@ -238,7 +254,7 @@ class Command(BaseCommand): and not sys.stdin.isatty() and select.select([sys.stdin], [], [], 0)[0] ): - exec(sys.stdin.read(), {**globals(), **self.get_namespace()}) + exec(sys.stdin.read(), {**globals(), **self.get_namespace(**options)}) return available_shells = ( diff --git a/tests/shell/tests.py b/tests/shell/tests.py index d8c708075d..49c85ecbe3 100644 --- a/tests/shell/tests.py +++ b/tests/shell/tests.py @@ -1,3 +1,5 @@ +import os +import subprocess import sys import unittest from unittest import mock @@ -23,25 +25,85 @@ class ShellCommandTestCase(SimpleTestCase): def test_command_option(self): with self.assertLogs("test", "INFO") as cm: - call_command( - "shell", - command=( - "import django; from logging import getLogger; " - 'getLogger("test").info(django.__version__)' - ), - ) + with captured_stdout(): + call_command( + "shell", + command=( + "import django; from logging import getLogger; " + 'getLogger("test").info(django.__version__)' + ), + ) self.assertEqual(cm.records[0].getMessage(), __version__) def test_command_option_globals(self): with captured_stdout() as stdout: - call_command("shell", command=self.script_globals) + call_command("shell", command=self.script_globals, verbosity=0) self.assertEqual(stdout.getvalue().strip(), "True") def test_command_option_inline_function_call(self): with captured_stdout() as stdout: - call_command("shell", command=self.script_with_inline_function) + call_command("shell", command=self.script_with_inline_function, verbosity=0) self.assertEqual(stdout.getvalue().strip(), __version__) + @override_settings(INSTALLED_APPS=["shell"]) + def test_no_settings(self): + test_environ = os.environ.copy() + if "DJANGO_SETTINGS_MODULE" in test_environ: + del test_environ["DJANGO_SETTINGS_MODULE"] + error = ( + "Automatic imports are disabled since settings are not configured.\n" + "DJANGO_SETTINGS_MODULE value is None.\n" + "HINT: Ensure that the settings module is configured and set.\n\n" + ) + for verbosity, assertError in [ + ("0", self.assertNotIn), + ("1", self.assertIn), + ("2", self.assertIn), + ]: + with self.subTest(verbosity=verbosity, get_auto_imports="models"): + p = subprocess.run( + [ + sys.executable, + "-m", + "django", + "shell", + "-c", + "print(globals())", + "-v", + verbosity, + ], + capture_output=True, + env=test_environ, + text=True, + umask=-1, + ) + assertError(error, p.stdout) + self.assertNotIn("Marker", p.stdout) + + with self.subTest(verbosity=verbosity, get_auto_imports="without-models"): + with mock.patch( + "django.core.management.commands.shell.Command.get_auto_imports", + return_value=["django.urls.resolve"], + ): + p = subprocess.run( + [ + sys.executable, + "-m", + "django", + "shell", + "-c", + "print(globals())", + "-v", + verbosity, + ], + capture_output=True, + env=test_environ, + text=True, + umask=-1, + ) + assertError(error, p.stdout) + self.assertNotIn("resolve", p.stdout) + @unittest.skipIf( sys.platform == "win32", "Windows select() doesn't support file descriptors." ) @@ -50,7 +112,7 @@ class ShellCommandTestCase(SimpleTestCase): with captured_stdin() as stdin, captured_stdout() as stdout: stdin.write("print(100)\n") stdin.seek(0) - call_command("shell") + call_command("shell", verbosity=0) self.assertEqual(stdout.getvalue().strip(), "100") @unittest.skipIf( @@ -62,7 +124,7 @@ class ShellCommandTestCase(SimpleTestCase): with captured_stdin() as stdin, captured_stdout() as stdout: stdin.write(self.script_globals) stdin.seek(0) - call_command("shell") + call_command("shell", verbosity=0) self.assertEqual(stdout.getvalue().strip(), "True") @unittest.skipIf( @@ -74,7 +136,7 @@ class ShellCommandTestCase(SimpleTestCase): with captured_stdin() as stdin, captured_stdout() as stdout: stdin.write(self.script_with_inline_function) stdin.seek(0) - call_command("shell") + call_command("shell", verbosity=0) self.assertEqual(stdout.getvalue().strip(), __version__) def test_ipython(self):