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):
"""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__

View file

@ -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

View file

@ -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
<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.
.. 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

View file

@ -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
~~~~~~~~~~

View file

@ -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):