Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed #35515 -- Added auto-importing to shell command. #18158

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
36 changes: 33 additions & 3 deletions django/core/management/commands/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import sys
import traceback

from django.apps import apps
from django.core.management import BaseCommand, CommandError
from django.utils.datastructures import OrderedSet

Expand Down Expand Up @@ -44,21 +45,35 @@ def add_arguments(self, parser):
),
)

def display_message(self, func):
salvo-polizzi marked this conversation as resolved.
Show resolved Hide resolved

def wrapper():
imported_objects = func()

self.stdout.write(
f"{len(imported_objects)} objects imported automatically",
self.style.SUCCESS,
)

return imported_objects

return wrapper

def ipython(self, options):
from IPython import start_ipython

start_ipython(argv=[])
start_ipython(argv=[], user_ns=self.display_message(self.get_namespace)())

def bpython(self, options):
import bpython

bpython.embed()
bpython.embed(self.display_message(self.get_namespace)())

def python(self, options):
import code

# Set up a dictionary to serve as the environment for the shell.
imported_objects = {}
imported_objects = self.display_message(self.get_namespace)()

# We want to honor both $PYTHONSTARTUP and .pythonrc.py, so follow system
# conventions and get $PYTHONSTARTUP first then .pythonrc.py.
Expand Down Expand Up @@ -111,7 +126,22 @@ def python(self, options):
# Start the interactive interpreter.
code.interact(local=imported_objects)

def get_namespace(self):
apps_models = apps.get_models()
imported_objects = {}
salvo-polizzi marked this conversation as resolved.
Show resolved Hide resolved
for model in reversed(apps_models):
salvo-polizzi marked this conversation as resolved.
Show resolved Hide resolved
if model.__module__:
imported_objects[model.__name__] = model
return imported_objects

def update_globals(self):
imported_objects = self.get_namespace()
for model_name, model in imported_objects.items():
globals()[model_name] = model
salvo-polizzi marked this conversation as resolved.
Show resolved Hide resolved

def handle(self, **options):
self.update_globals()

# Execute the command and exit.
if options["command"]:
exec(options["command"], globals())
Expand Down
12 changes: 12 additions & 0 deletions tests/shell/management/commands/overridden_shell.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from django.core.management.commands import shell


class Command(shell.Command):
def get_namespace(self):
from django.urls.base import resolve, reverse

return {
**super().get_namespace(),
"resolve": resolve,
"reverse": reverse,
}
9 changes: 9 additions & 0 deletions tests/shell/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from django.db import models


class Marker(models.Model):
pass


class Phone(models.Model):
name = models.CharField(max_length=50)
58 changes: 58 additions & 0 deletions tests/shell/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,22 @@
from unittest import mock

from django import __version__
from django.contrib.auth.models import User
from django.core.management import CommandError, call_command
from django.core.management.commands import shell
from django.test import SimpleTestCase
from django.test.utils import captured_stdin, captured_stdout

from .management.commands import overridden_shell


class ShellCommandTestCase(SimpleTestCase):
script_globals = 'print("__name__" in globals())'
script_globals_import = 'print("Phone" in globals())'
script_with_inline_function = (
"import django\ndef f():\n print(django.__version__)\nf()"
)
script_with_imports = 'p = Phone(name="name")\nprint(p.name)'

def test_command_option(self):
with self.assertLogs("test", "INFO") as cm:
Expand All @@ -35,6 +41,11 @@ def test_command_option_inline_function_call(self):
call_command("shell", command=self.script_with_inline_function)
self.assertEqual(stdout.getvalue().strip(), __version__)

def test_command_option_with_imports(self):
with captured_stdout() as stdout:
call_command("shell", command=self.script_with_imports)
self.assertEqual(stdout.getvalue().strip(), "name")

@unittest.skipIf(
sys.platform == "win32", "Windows select() doesn't support file descriptors."
)
Expand All @@ -58,6 +69,18 @@ def test_stdin_read_globals(self, select):
call_command("shell")
self.assertEqual(stdout.getvalue().strip(), "True")

@unittest.skipIf(
sys.platform == "win32",
"Windows select() doesn't support file descriptors.",
)
@mock.patch("django.core.management.commands.shell.select") # [1]
def test_stdin_read_globals_import(self, select):
with captured_stdin() as stdin, captured_stdout() as stdout:
stdin.write(self.script_globals_import)
salvo-polizzi marked this conversation as resolved.
Show resolved Hide resolved
stdin.seek(0)
call_command("shell")
self.assertEqual(stdout.getvalue().strip(), "True")

@unittest.skipIf(
sys.platform == "win32",
"Windows select() doesn't support file descriptors.",
Expand All @@ -70,6 +93,18 @@ def test_stdin_read_inline_function_call(self, select):
call_command("shell")
self.assertEqual(stdout.getvalue().strip(), __version__)

@unittest.skipIf(
sys.platform == "win32",
"Windows select() doesn't support file descriptors.",
)
@mock.patch("django.core.management.commands.shell.select")
def test_stdin_read_imports(self, select):
with captured_stdin() as stdin, captured_stdout() as stdout:
stdin.write(self.script_with_imports)
stdin.seek(0)
call_command("shell")
self.assertEqual(stdout.getvalue().strip(), "name")

@mock.patch("django.core.management.commands.shell.select.select") # [1]
@mock.patch.dict("sys.modules", {"IPython": None})
def test_shell_with_ipython_not_installed(self, select):
Expand All @@ -94,3 +129,26 @@ def test_shell_with_bpython_not_installed(self, select):
# returns EOF and so select always shows that sys.stdin is ready to read.
# This causes problems because of the call to select.select() toward the
# end of shell's handle() method.

def test_get_namespace(self):
from .models import Marker, Phone
salvo-polizzi marked this conversation as resolved.
Show resolved Hide resolved

cmd = shell.Command()
imports = cmd.get_namespace().values()

self.assertIn(Marker, imports)
self.assertIn(Phone, imports)
self.assertIn(User, imports)
salvo-polizzi marked this conversation as resolved.
Show resolved Hide resolved

def test_overridden_get_namespace(self):
from django.urls.base import resolve, reverse

from .models import Marker, Phone
salvo-polizzi marked this conversation as resolved.
Show resolved Hide resolved

cmd = overridden_shell.Command()
imports = cmd.get_namespace().values()

self.assertIn(Marker, imports)
self.assertIn(Phone, imports)
self.assertIn(resolve, imports)
self.assertIn(reverse, imports)