From fab52fc571c21e135f047a46bbdefab9d03a2909 Mon Sep 17 00:00:00 2001 From: Paul Schilling Date: Sat, 8 Jun 2024 18:40:25 +0200 Subject: [PATCH] Fixed #32915 -- Refactored runserver command to restore traceback The traceback for `runserver --nostatic` with broken/missing settings was hidden because the custom runserver command which added `--nostatic` to the command args was only instantiated if the settings had no errors. --- .../management/commands/runserver.py | 36 ------------------- django/core/management/commands/runserver.py | 27 ++++++++++++-- tests/admin_scripts/tests.py | 25 ++++++++++++- tests/settings_tests/tests.py | 6 ++++ tests/staticfiles_tests/test_management.py | 25 ++----------- 5 files changed, 57 insertions(+), 62 deletions(-) delete mode 100644 django/contrib/staticfiles/management/commands/runserver.py diff --git a/django/contrib/staticfiles/management/commands/runserver.py b/django/contrib/staticfiles/management/commands/runserver.py deleted file mode 100644 index fd9ddb16a4..0000000000 --- a/django/contrib/staticfiles/management/commands/runserver.py +++ /dev/null @@ -1,36 +0,0 @@ -from django.conf import settings -from django.contrib.staticfiles.handlers import StaticFilesHandler -from django.core.management.commands.runserver import Command as RunserverCommand - - -class Command(RunserverCommand): - help = ( - "Starts a lightweight web server for development and also serves static files." - ) - - def add_arguments(self, parser): - super().add_arguments(parser) - parser.add_argument( - "--nostatic", - action="store_false", - dest="use_static_handler", - help="Tells Django to NOT automatically serve static files at STATIC_URL.", - ) - parser.add_argument( - "--insecure", - action="store_true", - dest="insecure_serving", - help="Allows serving static files even if DEBUG is False.", - ) - - def get_handler(self, *args, **options): - """ - Return the static files serving handler wrapping the default handler, - if static files should be served. Otherwise return the default handler. - """ - handler = super().get_handler(*args, **options) - use_static_handler = options["use_static_handler"] - insecure_serving = options["insecure_serving"] - if use_static_handler and (settings.DEBUG or insecure_serving): - return StaticFilesHandler(handler) - return handler diff --git a/django/core/management/commands/runserver.py b/django/core/management/commands/runserver.py index 132ee4c079..a09cf4810e 100644 --- a/django/core/management/commands/runserver.py +++ b/django/core/management/commands/runserver.py @@ -6,6 +6,7 @@ import sys from datetime import datetime from django.conf import settings +from django.contrib.staticfiles.handlers import StaticFilesHandler from django.core.management.base import BaseCommand, CommandError from django.core.servers.basehttp import WSGIServer, get_internal_wsgi_application, run from django.db import connections @@ -65,6 +66,18 @@ class Command(BaseCommand): action="store_true", help="Skip system checks.", ) + parser.add_argument( + "--nostatic", + action="store_false", + dest="use_static_handler", + help="Tells Django to NOT automatically serve static files at STATIC_URL.", + ) + parser.add_argument( + "--insecure", + action="store_true", + dest="insecure_serving", + help="Allows serving static files even if DEBUG is False.", + ) def execute(self, *args, **options): if options["no_color"]: @@ -75,8 +88,18 @@ class Command(BaseCommand): super().execute(*args, **options) def get_handler(self, *args, **options): - """Return the default WSGI handler for the runner.""" - return get_internal_wsgi_application() + """ + Return the default WSGI handler for the runner, or `StaticFilesHandler` if + either of --nostatic/--insecure is passed + """ + handler = get_internal_wsgi_application() + + use_static_handler = options["use_static_handler"] + insecure_serving = options["insecure_serving"] + if use_static_handler and (settings.DEBUG or insecure_serving): + handler = StaticFilesHandler(handler) + + return handler def handle(self, *args, **options): if not settings.DEBUG and not settings.ALLOWED_HOSTS: diff --git a/tests/admin_scripts/tests.py b/tests/admin_scripts/tests.py index 2e77f2c97a..19ba3aac85 100644 --- a/tests/admin_scripts/tests.py +++ b/tests/admin_scripts/tests.py @@ -30,7 +30,13 @@ from django.core.management.commands.runserver import Command as RunserverComman from django.core.management.commands.testserver import Command as TestserverCommand from django.db import ConnectionHandler, connection from django.db.migrations.recorder import MigrationRecorder -from django.test import LiveServerTestCase, SimpleTestCase, TestCase, override_settings +from django.test import ( + LiveServerTestCase, + RequestFactory, + SimpleTestCase, + TestCase, + override_settings, +) from django.test.utils import captured_stderr, captured_stdout from django.urls import path from django.utils.version import PY313 @@ -1695,6 +1701,23 @@ class ManageRunserver(SimpleTestCase): self.assertIn("Performing system checks...", self.output.getvalue()) mocked_check.assert_called() + @override_settings(MIDDLEWARE=["django.middleware.common.CommonMiddleware"]) + def test_middleware_loaded_only_once(self): + with mock.patch("django.middleware.common.CommonMiddleware") as mocked: + self.cmd.get_handler(use_static_handler=True, insecure_serving=True) + self.assertEqual(mocked.call_count, 1) + + def test_404_response(self): + handler = self.cmd.get_handler(use_static_handler=True, insecure_serving=True) + missing_static_file = os.path.join(settings.STATIC_URL, "unknown.css") + req = RequestFactory().get(missing_static_file) + with override_settings(DEBUG=False): + response = handler.get_response(req) + self.assertEqual(response.status_code, 404) + with override_settings(DEBUG=True): + response = handler.get_response(req) + self.assertEqual(response.status_code, 404) + class ManageRunserverMigrationWarning(TestCase): def setUp(self): diff --git a/tests/settings_tests/tests.py b/tests/settings_tests/tests.py index 4fc35689d6..15539bc1e8 100644 --- a/tests/settings_tests/tests.py +++ b/tests/settings_tests/tests.py @@ -338,6 +338,12 @@ class SettingsTests(SimpleTestCase): with self.assertRaisesMessage(ValueError, "Incorrect timezone setting: test"): settings._setup() + def test_import_non_existing_module(self): + exc = ModuleNotFoundError("No module named 'fake_module'", name="fake_module") + with mock.patch("importlib.import_module", side_effect=exc): + with self.assertRaisesMessage(ImportError, "No module named 'fake_module'"): + Settings("fake_settings_module") + class TestComplexSettingOverride(SimpleTestCase): def setUp(self): diff --git a/tests/staticfiles_tests/test_management.py b/tests/staticfiles_tests/test_management.py index c0d3817383..32a5a6c001 100644 --- a/tests/staticfiles_tests/test_management.py +++ b/tests/staticfiles_tests/test_management.py @@ -11,11 +11,11 @@ from admin_scripts.tests import AdminScriptTestCase from django.conf import STATICFILES_STORAGE_ALIAS, settings from django.contrib.staticfiles import storage -from django.contrib.staticfiles.management.commands import collectstatic, runserver +from django.contrib.staticfiles.management.commands import collectstatic from django.core.exceptions import ImproperlyConfigured from django.core.management import CommandError, call_command from django.core.management.base import SystemCheckError -from django.test import RequestFactory, override_settings +from django.test import override_settings from django.test.utils import extend_sys_path from django.utils._os import symlinks_supported from django.utils.functional import empty @@ -33,27 +33,6 @@ class TestNoFilesCreated: self.assertEqual(os.listdir(settings.STATIC_ROOT), []) -class TestRunserver(StaticFilesTestCase): - @override_settings(MIDDLEWARE=["django.middleware.common.CommonMiddleware"]) - def test_middleware_loaded_only_once(self): - command = runserver.Command() - with mock.patch("django.middleware.common.CommonMiddleware") as mocked: - command.get_handler(use_static_handler=True, insecure_serving=True) - self.assertEqual(mocked.call_count, 1) - - def test_404_response(self): - command = runserver.Command() - handler = command.get_handler(use_static_handler=True, insecure_serving=True) - missing_static_file = os.path.join(settings.STATIC_URL, "unknown.css") - req = RequestFactory().get(missing_static_file) - with override_settings(DEBUG=False): - response = handler.get_response(req) - self.assertEqual(response.status_code, 404) - with override_settings(DEBUG=True): - response = handler.get_response(req) - self.assertEqual(response.status_code, 404) - - class TestFindStatic(TestDefaults, CollectionTestCase): """ Test ``findstatic`` management command.