From a5cd84ad2002f9a43363ab9fd9d9f6e9dfa48c60 Mon Sep 17 00:00:00 2001 From: Salvo Polizzi Date: Fri, 14 Feb 2025 08:17:25 +0100 Subject: [PATCH] Fixed #35680 -- Added automatic imports of common utilies to shell management command. --- django/core/management/commands/shell.py | 22 +++++-- docs/howto/custom-shell.txt | 17 +++-- docs/ref/django-admin.txt | 17 ++++- docs/releases/6.0.txt | 3 + tests/shell/tests.py | 80 +++++++++++++++++++++--- 5 files changed, 119 insertions(+), 20 deletions(-) diff --git a/django/core/management/commands/shell.py b/django/core/management/commands/shell.py index 132e7f89d2..25edc80f7c 100644 --- a/django/core/management/commands/shell.py +++ b/django/core/management/commands/shell.py @@ -124,13 +124,19 @@ class Command(BaseCommand): def get_auto_imports(self): """Return a sequence of import paths for objects to be auto-imported. - By default, import paths for models in INSTALLED_APPS are included, - with models from earlier apps taking precedence in case of a name - collision. + By default, import paths for models in INSTALLED_APPS and some common + utilities are included, with models from earlier apps taking precedence + in case of a name collision. For example, for an unchanged INSTALLED_APPS, this method returns: [ + "django.conf.settings", + "django.db.connection", + "django.db.reset_queries", + "django.db.models", + "django.db.models.functions", + "django.utils.timezone", "django.contrib.sessions.models.Session", "django.contrib.contenttypes.models.ContentType", "django.contrib.auth.models.User", @@ -140,7 +146,15 @@ class Command(BaseCommand): ] """ - app_models_imports = [ + default_imports = [ + "django.conf.settings", + "django.db.connection", + "django.db.reset_queries", + "django.db.models", + "django.db.models.functions", + "django.utils.timezone", + ] + app_models_imports = default_imports + [ f"{model.__module__}.{model.__name__}" for model in reversed(apps.get_models()) if model.__module__ diff --git a/docs/howto/custom-shell.txt b/docs/howto/custom-shell.txt index c1632849b5..cf55a02400 100644 --- a/docs/howto/custom-shell.txt +++ b/docs/howto/custom-shell.txt @@ -39,20 +39,29 @@ For example: The customization above adds :func:`~django.urls.resolve` and :func:`~django.urls.reverse` to the default namespace, which already includes -all models from the apps listed in :setting:`INSTALLED_APPS`. These objects -will be available in the ``shell`` without requiring a manual import. +all models from the apps listed in :setting:`INSTALLED_APPS` plus what is +imported by default. These objects will be available in the ``shell`` without +requiring a manual import. Running this customized ``shell`` command with ``verbosity=2`` would show: .. console:: - 8 objects imported automatically: - + 13 objects imported automatically: + + from django.db import connection, reset_queries, models + from django.conf import settings from django.contrib.admin.models import LogEntry from django.contrib.auth.models import Group, Permission, User from django.contrib.contenttypes.models import ContentType from django.contrib.sessions.models import Session from django.urls import resolve, reverse + from django.utils import timezone + +.. versionchanged:: 6.0 + + Automatic imports of common utilities, such as ``django.conf.settings``, + were added. If an overridden ``shell`` command includes paths that cannot be imported, these errors are shown when ``verbosity`` is set to ``1`` or higher. Duplicate diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index a1603c9e84..22a7c0a10c 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -1067,9 +1067,15 @@ Starts the Python interactive interpreter. All models from installed apps are automatically imported into the shell environment. Models from apps listed earlier in :setting:`INSTALLED_APPS` take -precedence. For a ``--verbosity`` of 2 or higher, the automatically imported -objects will be listed. To disable automatic importing entirely, use the -``--no-imports`` flag. +precedence. The following common utilities are also imported:: + + from django.db import connection, reset_queries, models + from django.conf import settings + from django.utils import timezone + +For a ``--verbosity`` of 2 or higher, the automatically imported objects will +be listed. To disable automatic importing entirely, use the ``--no-imports`` +flag. See the guide on :ref:`customizing this behavior ` to add or remove automatic imports. @@ -1078,6 +1084,11 @@ See the guide on :ref:`customizing this behavior Automatic models import was added. +.. versionchanged:: 6.0 + + Automatic imports of common utilities, such as ``django.conf.settings``, + were added. + .. django-admin-option:: --interface {ipython,bpython,python}, -i {ipython,bpython,python} Specifies the shell to use. By default, Django will use IPython_ or bpython_ if diff --git a/docs/releases/6.0.txt b/docs/releases/6.0.txt index 6a819ecc3f..a7306ba755 100644 --- a/docs/releases/6.0.txt +++ b/docs/releases/6.0.txt @@ -224,6 +224,9 @@ Management Commands * The :djadmin:`startproject` and :djadmin:`startapp` commands now create the custom target directory if it doesn't exist. +* Common utilities, such as ``django.conf.settings``, are now automatically + imported to the :djadmin:`shell` by default. + Migrations ~~~~~~~~~~ diff --git a/tests/shell/tests.py b/tests/shell/tests.py index ff4db089f7..8ffbc3fa4f 100644 --- a/tests/shell/tests.py +++ b/tests/shell/tests.py @@ -5,14 +5,17 @@ import unittest from unittest import mock from django import __version__ +from django.conf import settings from django.contrib.auth.models import Group, Permission, User from django.contrib.contenttypes.models import ContentType from django.core.management import CommandError, call_command from django.core.management.commands import shell -from django.db import connection +from django.db import connection, models, reset_queries +from django.db.models import functions from django.test import SimpleTestCase from django.test.utils import captured_stdin, captured_stdout, override_settings from django.urls import resolve, reverse +from django.utils import timezone from .models import Marker, Phone @@ -79,6 +82,8 @@ class ShellCommandTestCase(SimpleTestCase): ) assertError(error, p.stdout) self.assertNotIn("Marker", p.stdout) + self.assertNotIn("reset_queries", p.stdout) + self.assertNotIn("imported automatically", p.stdout) with self.subTest(verbosity=verbosity, get_auto_imports="without-models"): with mock.patch( @@ -214,6 +219,12 @@ class ShellCommandAutoImportsTestCase(SimpleTestCase): self.assertEqual( namespace, { + "settings": settings, + "connection": connection, + "reset_queries": reset_queries, + "models": models, + "functions": functions, + "timezone": timezone, "Marker": Marker, "Phone": Phone, "ContentType": ContentType, @@ -223,6 +234,22 @@ class ShellCommandAutoImportsTestCase(SimpleTestCase): }, ) + @override_settings(INSTALLED_APPS=[]) + def test_get_namespace_default_imports(self): + namespace = shell.Command().get_namespace() + + self.assertEqual( + namespace, + { + "settings": settings, + "connection": connection, + "reset_queries": reset_queries, + "models": models, + "functions": functions, + "timezone": timezone, + }, + ) + @override_settings( INSTALLED_APPS=["model_forms", "contenttypes_tests", "forms_tests"] ) @@ -243,7 +270,6 @@ class ShellCommandAutoImportsTestCase(SimpleTestCase): return super().get_auto_imports() + [ "django.urls.reverse", "django.urls.resolve", - "django.db.connection", ] namespace = TestCommand().get_namespace() @@ -251,9 +277,14 @@ class ShellCommandAutoImportsTestCase(SimpleTestCase): self.assertEqual( namespace, { - "connection": connection, "resolve": resolve, "reverse": reverse, + "settings": settings, + "connection": connection, + "reset_queries": reset_queries, + "models": models, + "functions": functions, + "timezone": timezone, "Marker": Marker, "Phone": Phone, "ContentType": ContentType, @@ -295,7 +326,7 @@ class ShellCommandAutoImportsTestCase(SimpleTestCase): self.assertEqual(len(namespace), len(cmd.get_auto_imports())) self.assertEqual( stdout.getvalue().strip(), - "6 objects imported automatically (use -v 2 for details).", + "12 objects imported automatically (use -v 2 for details).", ) @override_settings(INSTALLED_APPS=["shell", "django.contrib.contenttypes"]) @@ -320,9 +351,13 @@ class ShellCommandAutoImportsTestCase(SimpleTestCase): self.assertEqual( stdout.getvalue().strip(), - "7 objects imported automatically:\n\n" + "13 objects imported automatically:\n\n" " import shell\n" " import django\n" + " from django.conf import settings\n" + " from django.db import connection, reset_queries, models\n" + " from django.db.models import functions\n" + " from django.utils import timezone\n" " from django.contrib.contenttypes.models import ContentType\n" " from shell.models import Phone, Marker\n" " from django.urls import reverse, resolve", @@ -350,13 +385,36 @@ class ShellCommandAutoImportsTestCase(SimpleTestCase): TestCommand().get_namespace(verbosity=verbosity) self.assertEqual(stdout.getvalue().strip(), expected) - @override_settings(INSTALLED_APPS=[]) - def test_message_with_stdout_no_installed_apps(self): + def test_message_with_stdout_zero_object(self): + class TestCommand(shell.Command): + def get_auto_imports(self): + return [] + + with captured_stdout() as stdout: + TestCommand().get_namespace(verbosity=2) + cases = { 0: "", 1: "0 objects imported automatically.", 2: "0 objects imported automatically.", } + for verbosity, expected in cases.items(): + with self.subTest(verbosity=verbosity): + with captured_stdout() as stdout: + TestCommand().get_namespace(verbosity=verbosity) + self.assertEqual(stdout.getvalue().strip(), expected) + + @override_settings(INSTALLED_APPS=[]) + def test_message_with_stdout_no_installed_apps(self): + cases = { + 0: "", + 1: "6 objects imported automatically (use -v 2 for details).", + 2: "6 objects imported automatically:\n\n" + " from django.conf import settings\n" + " from django.db import connection, reset_queries, models\n" + " from django.db.models import functions\n" + " from django.utils import timezone", + } for verbosity, expected in cases.items(): with self.subTest(verbosity=verbosity): with captured_stdout() as stdout: @@ -379,7 +437,11 @@ class ShellCommandAutoImportsTestCase(SimpleTestCase): def test_message_with_stdout_listing_objects_with_isort(self): sorted_imports = ( " from shell.models import Marker, Phone\n\n" - " from django.contrib.contenttypes.models import ContentType" + " from django.db import connection, models, reset_queries\n" + " from django.db.models import functions\n" + " from django.contrib.contenttypes.models import ContentType\n" + " from django.conf import settings\n" + " from django.utils import timezone" ) mock_isort_code = mock.Mock(code=mock.MagicMock(return_value=sorted_imports)) @@ -399,7 +461,7 @@ class ShellCommandAutoImportsTestCase(SimpleTestCase): self.assertEqual( stdout.getvalue().strip(), - "6 objects imported automatically:\n\n" + sorted_imports, + "12 objects imported automatically:\n\n" + sorted_imports, ) def test_override_get_auto_imports(self):