From ac7f7eab0fad18fffc465bc4fdc092bcb2de2d80 Mon Sep 17 00:00:00 2001 From: Tom Forbes Date: Thu, 18 Jun 2020 12:04:10 +0100 Subject: [PATCH] [3.1.x] Fixed #31716 -- Fixed detection of console scripts in autoreloader on Windows. Backport of 8a902b7ee622ada258d15fb122092c1f02b82698 from master --- django/utils/autoreload.py | 16 ++++++++- tests/utils_tests/test_autoreload.py | 53 ++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/django/utils/autoreload.py b/django/utils/autoreload.py index 845c46c8d44..1ef10a15f92 100644 --- a/django/utils/autoreload.py +++ b/django/utils/autoreload.py @@ -207,12 +207,26 @@ def get_child_arguments(): on reloading. """ import django.__main__ + django_main_path = Path(django.__main__.__file__) + py_script = Path(sys.argv[0]) args = [sys.executable] + ['-W%s' % o for o in sys.warnoptions] - if sys.argv[0] == django.__main__.__file__: + if py_script == django_main_path: # The server was started with `python -m django runserver`. args += ['-m', 'django'] args += sys.argv[1:] + elif not py_script.exists(): + # sys.argv[0] may not exist for several reasons on Windows. + # It may exist with a .exe extension or have a -script.py suffix. + exe_entrypoint = py_script.with_suffix('.exe') + if exe_entrypoint.exists(): + # Should be executed directly, ignoring sys.executable. + return [exe_entrypoint, *sys.argv[1:]] + script_entrypoint = py_script.with_name('%s-script.py' % py_script.name) + if script_entrypoint.exists(): + # Should be executed as usual. + return [*args, script_entrypoint, *sys.argv[1:]] + raise RuntimeError('Script %s does not exist.' % py_script) else: args += sys.argv return args diff --git a/tests/utils_tests/test_autoreload.py b/tests/utils_tests/test_autoreload.py index 09656b8599e..f4ecb387777 100644 --- a/tests/utils_tests/test_autoreload.py +++ b/tests/utils_tests/test_autoreload.py @@ -14,6 +14,7 @@ from pathlib import Path from subprocess import CompletedProcess from unittest import mock, skip, skipIf +import django.__main__ from django.apps.registry import Apps from django.test import SimpleTestCase from django.test.utils import extend_sys_path @@ -153,6 +154,55 @@ class TestIterModulesAndFiles(SimpleTestCase): ) +class TestChildArguments(SimpleTestCase): + @mock.patch('sys.argv', [django.__main__.__file__, 'runserver']) + @mock.patch('sys.warnoptions', []) + def test_run_as_module(self): + self.assertEqual( + autoreload.get_child_arguments(), + [sys.executable, '-m', 'django', 'runserver'] + ) + + @mock.patch('sys.argv', [__file__, 'runserver']) + @mock.patch('sys.warnoptions', ['error']) + def test_warnoptions(self): + self.assertEqual( + autoreload.get_child_arguments(), + [sys.executable, '-Werror', __file__, 'runserver'] + ) + + @mock.patch('sys.warnoptions', []) + def test_exe_fallback(self): + tmpdir = tempfile.TemporaryDirectory() + self.addCleanup(tmpdir.cleanup) + exe_path = Path(tmpdir.name) / 'django-admin.exe' + exe_path.touch() + with mock.patch('sys.argv', [exe_path.with_suffix(''), 'runserver']): + self.assertEqual( + autoreload.get_child_arguments(), + [exe_path, 'runserver'] + ) + + @mock.patch('sys.warnoptions', []) + def test_entrypoint_fallback(self): + tmpdir = tempfile.TemporaryDirectory() + self.addCleanup(tmpdir.cleanup) + script_path = Path(tmpdir.name) / 'django-admin-script.py' + script_path.touch() + with mock.patch('sys.argv', [script_path.with_name('django-admin'), 'runserver']): + self.assertEqual( + autoreload.get_child_arguments(), + [sys.executable, script_path, 'runserver'] + ) + + @mock.patch('sys.argv', ['does-not-exist', 'runserver']) + @mock.patch('sys.warnoptions', []) + def test_raises_runtimeerror(self): + msg = 'Script does-not-exist does not exist.' + with self.assertRaisesMessage(RuntimeError, msg): + autoreload.get_child_arguments() + + class TestCommonRoots(SimpleTestCase): def test_common_roots(self): paths = ( @@ -360,6 +410,9 @@ class RestartWithReloaderTests(SimpleTestCase): return mock_call def test_manage_py(self): + script = Path('manage.py') + script.touch() + self.addCleanup(script.unlink) argv = ['./manage.py', 'runserver'] mock_call = self.patch_autoreload(argv) autoreload.restart_with_reloader()