diff --git a/django/core/management/commands/shell.py b/django/core/management/commands/shell.py index b4e9caf0d1f..9cc40902f4a 100644 --- a/django/core/management/commands/shell.py +++ b/django/core/management/commands/shell.py @@ -1,4 +1,6 @@ import os +import select +import sys import warnings from django.core.management.base import BaseCommand @@ -7,7 +9,12 @@ from django.utils.deprecation import RemovedInDjango20Warning class Command(BaseCommand): - help = "Runs a Python interactive interpreter. Tries to use IPython or bpython, if one of them is available." + help = ( + "Runs a Python interactive interpreter. Tries to use IPython or " + "bpython, if one of them is available. Any standard input is executed " + "as code." + ) + requires_system_checks = False shells = ['ipython', 'bpython', 'python'] @@ -114,6 +121,12 @@ class Command(BaseCommand): exec(options['command']) return + # Execute stdin if it has anything to read and exit. + # Not supported on Windows due to select.select() limitations. + if sys.platform != 'win32' and select.select([sys.stdin], [], [], 0)[0]: + exec(sys.stdin.read()) + return + available_shells = [options['interface']] if options['interface'] else self.shells for shell in available_shells: diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index c35e728f264..7a77f4d1e7d 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -979,6 +979,22 @@ Lets you pass a command as a string to execute it as Django, like so:: django-admin shell --command="import django; print(django.__version__)" +You can also pass code in on standard input to execute it. For example: + +.. code-block:: console + + $ django-admin shell < import django + > print(django.__version__) + > EOF + +On Windows, the REPL is output due to implementation limits of +:func:`select.select` on that platform. + +.. versionchanged:: 1.11 + + In older versions, the REPL is also output on UNIX systems. + ``showmigrations`` ------------------ diff --git a/tests/shell/tests.py b/tests/shell/tests.py index 7285c968a25..11f1826d30e 100644 --- a/tests/shell/tests.py +++ b/tests/shell/tests.py @@ -1,7 +1,10 @@ +import sys +import unittest + from django import __version__ from django.core.management import call_command -from django.test import SimpleTestCase -from django.test.utils import patch_logger +from django.test import SimpleTestCase, mock +from django.test.utils import captured_stdin, captured_stdout, patch_logger class ShellCommandTestCase(SimpleTestCase): @@ -17,3 +20,12 @@ class ShellCommandTestCase(SimpleTestCase): ) self.assertEqual(len(logger), 1) self.assertEqual(logger[0], __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(self, select): + with captured_stdin() as stdin, captured_stdout() as stdout: + stdin.write('print(100)\n') + stdin.seek(0) + call_command('shell') + self.assertEqual(stdout.getvalue().strip(), '100')