1
0
mirror of https://github.com/django/django.git synced 2025-08-21 01:09:13 +00:00

Fixed #35680 -- Added automatic imports of common utilies to shell management command.

This commit is contained in:
Salvo Polizzi 2025-02-14 08:17:25 +01:00 committed by Sarah Boyce
parent 8499fba0e1
commit a5cd84ad20
5 changed files with 119 additions and 20 deletions

View File

@ -124,13 +124,19 @@ class Command(BaseCommand):
def get_auto_imports(self): def get_auto_imports(self):
"""Return a sequence of import paths for objects to be auto-imported. """Return a sequence of import paths for objects to be auto-imported.
By default, import paths for models in INSTALLED_APPS are included, By default, import paths for models in INSTALLED_APPS and some common
with models from earlier apps taking precedence in case of a name utilities are included, with models from earlier apps taking precedence
collision. in case of a name collision.
For example, for an unchanged INSTALLED_APPS, this method returns: 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.sessions.models.Session",
"django.contrib.contenttypes.models.ContentType", "django.contrib.contenttypes.models.ContentType",
"django.contrib.auth.models.User", "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__}" f"{model.__module__}.{model.__name__}"
for model in reversed(apps.get_models()) for model in reversed(apps.get_models())
if model.__module__ if model.__module__

View File

@ -39,20 +39,29 @@ For example:
The customization above adds :func:`~django.urls.resolve` and The customization above adds :func:`~django.urls.resolve` and
:func:`~django.urls.reverse` to the default namespace, which already includes :func:`~django.urls.reverse` to the default namespace, which already includes
all models from the apps listed in :setting:`INSTALLED_APPS`. These objects all models from the apps listed in :setting:`INSTALLED_APPS` plus what is
will be available in the ``shell`` without requiring a manual import. 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: Running this customized ``shell`` command with ``verbosity=2`` would show:
.. console:: .. 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.admin.models import LogEntry
from django.contrib.auth.models import Group, Permission, User from django.contrib.auth.models import Group, Permission, User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.sessions.models import Session from django.contrib.sessions.models import Session
from django.urls import resolve, reverse 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, If an overridden ``shell`` command includes paths that cannot be imported,
these errors are shown when ``verbosity`` is set to ``1`` or higher. Duplicate these errors are shown when ``verbosity`` is set to ``1`` or higher. Duplicate

View File

@ -1067,9 +1067,15 @@ Starts the Python interactive interpreter.
All models from installed apps are automatically imported into the shell All models from installed apps are automatically imported into the shell
environment. Models from apps listed earlier in :setting:`INSTALLED_APPS` take environment. Models from apps listed earlier in :setting:`INSTALLED_APPS` take
precedence. For a ``--verbosity`` of 2 or higher, the automatically imported precedence. The following common utilities are also imported::
objects will be listed. To disable automatic importing entirely, use the
``--no-imports`` flag. 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 See the guide on :ref:`customizing this behavior
<customizing-shell-auto-imports>` to add or remove automatic imports. <customizing-shell-auto-imports>` to add or remove automatic imports.
@ -1078,6 +1084,11 @@ See the guide on :ref:`customizing this behavior
Automatic models import was added. 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} .. 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 Specifies the shell to use. By default, Django will use IPython_ or bpython_ if

View File

@ -224,6 +224,9 @@ Management Commands
* The :djadmin:`startproject` and :djadmin:`startapp` commands now create the * The :djadmin:`startproject` and :djadmin:`startapp` commands now create the
custom target directory if it doesn't exist. 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 Migrations
~~~~~~~~~~ ~~~~~~~~~~

View File

@ -5,14 +5,17 @@ import unittest
from unittest import mock from unittest import mock
from django import __version__ from django import __version__
from django.conf import settings
from django.contrib.auth.models import Group, Permission, User from django.contrib.auth.models import Group, Permission, User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.management import CommandError, call_command from django.core.management import CommandError, call_command
from django.core.management.commands import shell 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 import SimpleTestCase
from django.test.utils import captured_stdin, captured_stdout, override_settings from django.test.utils import captured_stdin, captured_stdout, override_settings
from django.urls import resolve, reverse from django.urls import resolve, reverse
from django.utils import timezone
from .models import Marker, Phone from .models import Marker, Phone
@ -79,6 +82,8 @@ class ShellCommandTestCase(SimpleTestCase):
) )
assertError(error, p.stdout) assertError(error, p.stdout)
self.assertNotIn("Marker", 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 self.subTest(verbosity=verbosity, get_auto_imports="without-models"):
with mock.patch( with mock.patch(
@ -214,6 +219,12 @@ class ShellCommandAutoImportsTestCase(SimpleTestCase):
self.assertEqual( self.assertEqual(
namespace, namespace,
{ {
"settings": settings,
"connection": connection,
"reset_queries": reset_queries,
"models": models,
"functions": functions,
"timezone": timezone,
"Marker": Marker, "Marker": Marker,
"Phone": Phone, "Phone": Phone,
"ContentType": ContentType, "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( @override_settings(
INSTALLED_APPS=["model_forms", "contenttypes_tests", "forms_tests"] INSTALLED_APPS=["model_forms", "contenttypes_tests", "forms_tests"]
) )
@ -243,7 +270,6 @@ class ShellCommandAutoImportsTestCase(SimpleTestCase):
return super().get_auto_imports() + [ return super().get_auto_imports() + [
"django.urls.reverse", "django.urls.reverse",
"django.urls.resolve", "django.urls.resolve",
"django.db.connection",
] ]
namespace = TestCommand().get_namespace() namespace = TestCommand().get_namespace()
@ -251,9 +277,14 @@ class ShellCommandAutoImportsTestCase(SimpleTestCase):
self.assertEqual( self.assertEqual(
namespace, namespace,
{ {
"connection": connection,
"resolve": resolve, "resolve": resolve,
"reverse": reverse, "reverse": reverse,
"settings": settings,
"connection": connection,
"reset_queries": reset_queries,
"models": models,
"functions": functions,
"timezone": timezone,
"Marker": Marker, "Marker": Marker,
"Phone": Phone, "Phone": Phone,
"ContentType": ContentType, "ContentType": ContentType,
@ -295,7 +326,7 @@ class ShellCommandAutoImportsTestCase(SimpleTestCase):
self.assertEqual(len(namespace), len(cmd.get_auto_imports())) self.assertEqual(len(namespace), len(cmd.get_auto_imports()))
self.assertEqual( self.assertEqual(
stdout.getvalue().strip(), 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"]) @override_settings(INSTALLED_APPS=["shell", "django.contrib.contenttypes"])
@ -320,9 +351,13 @@ class ShellCommandAutoImportsTestCase(SimpleTestCase):
self.assertEqual( self.assertEqual(
stdout.getvalue().strip(), stdout.getvalue().strip(),
"7 objects imported automatically:\n\n" "13 objects imported automatically:\n\n"
" import shell\n" " import shell\n"
" import django\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 django.contrib.contenttypes.models import ContentType\n"
" from shell.models import Phone, Marker\n" " from shell.models import Phone, Marker\n"
" from django.urls import reverse, resolve", " from django.urls import reverse, resolve",
@ -350,13 +385,36 @@ class ShellCommandAutoImportsTestCase(SimpleTestCase):
TestCommand().get_namespace(verbosity=verbosity) TestCommand().get_namespace(verbosity=verbosity)
self.assertEqual(stdout.getvalue().strip(), expected) self.assertEqual(stdout.getvalue().strip(), expected)
@override_settings(INSTALLED_APPS=[]) def test_message_with_stdout_zero_object(self):
def test_message_with_stdout_no_installed_apps(self): class TestCommand(shell.Command):
def get_auto_imports(self):
return []
with captured_stdout() as stdout:
TestCommand().get_namespace(verbosity=2)
cases = { cases = {
0: "", 0: "",
1: "0 objects imported automatically.", 1: "0 objects imported automatically.",
2: "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(): for verbosity, expected in cases.items():
with self.subTest(verbosity=verbosity): with self.subTest(verbosity=verbosity):
with captured_stdout() as stdout: with captured_stdout() as stdout:
@ -379,7 +437,11 @@ class ShellCommandAutoImportsTestCase(SimpleTestCase):
def test_message_with_stdout_listing_objects_with_isort(self): def test_message_with_stdout_listing_objects_with_isort(self):
sorted_imports = ( sorted_imports = (
" from shell.models import Marker, Phone\n\n" " 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)) mock_isort_code = mock.Mock(code=mock.MagicMock(return_value=sorted_imports))
@ -399,7 +461,7 @@ class ShellCommandAutoImportsTestCase(SimpleTestCase):
self.assertEqual( self.assertEqual(
stdout.getvalue().strip(), 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): def test_override_get_auto_imports(self):