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 #27600 -- Improved the way the 'shell' command reads python code from stdin. #7681
Conversation
Is it needed given things like https://code.djangoproject.com/ticket/25680#comment:3 ? Anyway, all features like this need a ticket and of course, documentation and tests (if feasible). |
Well it's not the same, this sorts of works:
That's better:
Thanks ! |
I removed the requirement to pass Is it ok if I wait until the ticket is accepted to write documentation and a test ? |
shell -c -
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems useful, especially since it's what python
does
@@ -114,6 +116,11 @@ def handle(self, **options): | |||
exec(options['command']) | |||
return | |||
|
|||
if select.select([sys.stdin], [], [], 0.0)[0]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
0.0 -> 0
Also what about windows support? select
module docs say at the top "Note that on Windows, it only works for sockets;" and under select
func, "a straightforward interface to the Unix select() system call.". This SO post implies it's not possible to use select
either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test passing seems to indicate that select()
will ignore file handlers on windows, and basically the interpreter will never go inside that if block on windows.
So, it doesn't seem to break when being ran on windows - I'm really not a windows expert - but it's never going to work, seems like on windows, select is just going to ignore the stdin file handler and thus not return that it has data for reading.
@@ -114,6 +116,11 @@ def handle(self, **options): | |||
exec(options['command']) | |||
return | |||
|
|||
if select.select([sys.stdin], [], [], 0.0)[0]: | |||
# Execute stdin and exit |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
comment before the if
?
I imagine it could be useful in a shell script to be able to do |
help = ( | ||
"Runs Python code passed through standard input if any otherwise " | ||
"runs an interactive interpreter, like the python CLI. " | ||
"It tries to use Tries to use IPython or bpython, if one of them is available." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the 'passed through standard input' is the most minor feature of the command, so it should come at the end of the help. Maybe take the original description and just add a sentence on the end about it instead?
@@ -17,3 +19,13 @@ def test_command_option(self): | |||
) | |||
self.assertEqual(len(logger), 1) | |||
self.assertEqual(logger[0], __version__) | |||
|
|||
@mock.patch('django.core.management.commands.shell.select') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these aren't necessary, you can pass StringIO
s for stdout
and stdin
to call_command
instead. there are lots of examples in existing tests e.g. https://github.com/django/django/blob/master/tests/i18n/test_compilation.py#L41
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see how we could possibly pass an instance of OutputWrapper to exec()
, so passing stdout to call_command has just no effect.
For stdin, I did the same hack as in createsuperuser so it's more consistent. But if you want my own opinion, I would discourage adding production code that's supposed to be run only in the test environment, I thought this was commonly known as a bad practice but am not sure anymore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aha I found what you should be using for both: django.utils.captured_stdin
and django.utils.captured_stdout
😉
You could also open a second PR removing the hack from createsuperuser
and change its tests to use captured_stdin
👍 Presumably
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ohhh right, thanks a heap Adam, it works great 🎸
Thanks @adamchainz ! Added test and documentation, but the windows test failed because of a fetch error, i'm curious to read what it would have logged if it had actually been executed. Agreed that being able to pipe data is pretty cool, not sure if it has any use on windows installations without something like cygwin ? |
Thanks @adamchainz for your feedback 🎸 Initially i have tried mocking as little as possible, and have grepped other tests for stdin/stdout to see if they where mocked and how it was done, so I tried using the stdout wrapper. The thing is that this wrapper is not used by the
stdout will be empty, because
|
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" | ||
"Like the python CLI, it would read and execute code passed to standard input if any and then exit." |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The 'would' sounds a bit out of place. Also the 'like the python CLI' sounds a little unnecessary now I read it again. Maybe "If there is standard input, it will be immediately executed as code and then the command will exit.".
|
||
call_command( | ||
'shell', | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no need for breaking this over multiple lines now
@@ -114,6 +119,11 @@ def handle(self, **options): | |||
exec(options['command']) | |||
return | |||
|
|||
# Execute stdin if it has anything to read and exit | |||
if select.select([self.stdin], [], [], 0)[0]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs changing back to sys.stdin
, it failed on Jenkins like
File "C:\Jenkins\workspace\pull-requests-windows\database\sqlite3\label\windows\python\Python27\django\core\management\commands\shell.py", line 123, in handle
if select.select([self.stdin], [], [], 0)[0]:
AttributeError: 'Command' object has no attribute 'stdin'
LGTM apart from a release note in |
@adamchainz thanks a heap mate, you 🎸 |
Tested on Windows using Git bash:
|
@@ -7,7 +9,11 @@ | |||
|
|||
|
|||
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" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrap lines at 79 characters. Suggested wording:
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."
)
@@ -314,6 +314,9 @@ Management Commands | |||
* The new :option:`diffsettings --default` option allows specifying a settings | |||
module other than Django's default settings to compare against. | |||
|
|||
* Improved the way the :djadmin:`shell` command reads python code from standard |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure a release note is needed -- it's not really clear to me what it means. Improved? How so? If some doc update were made in ref/django-admin.txt
to describe the new ability, then you could link to it from here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jpic add this link
with captured_stdin() as stdin, captured_stdout() as stdout: | ||
stdin.write('print(100)\n') | ||
stdin.seek(0) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
chop blank line?
Thanks @timgraham ! it wasn't expected to work on windows because its select() implementation doesn't support file descriptors, what's your recommendation here ? Is it possible to just add linux support for now and wait for someone to contribute windows support or should I try to get this to work on windows or something else perhaps ? |
I don't have any Windows expertise to offer. If the limitation is documented and perhaps if a more helpful exception could be raised, I wouldn't block this patch on that. |
Can't you detect windows and not even try |
Thanks for your replies, I changed the patch to "if sys.platform != 'win32' and select.select..." but I'm not sure if I need to change tests too, not sure about a lot of things so I'm downloading a windows VM and try to gather more information by experimenting myself, and start some triangulation around what's best to change in docs. |
@@ -18,6 +20,7 @@ def test_command_option(self): | |||
self.assertEqual(len(logger), 1) | |||
self.assertEqual(logger[0], __version__) | |||
|
|||
@unittest.skipIf(sys.platform == 'win32', "Python on Windows doesn't have working select().") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not exactly true, its select() works, but not on file objects like stdin
It's better to avoid pushing many small commits as each one starts a new build on the CI server. For a small patch like this, amending/force pushing a single commit is fine. |
Thanks @timgraham, good to know ! but if i amend won't it change the sha1
and thus cause a new build anyway ? On travis we can add '[ci skip]' to the
commit message to prevent triggering a build in this kind of cases.
|
The workflow I use is to address all review comments locally, then push to GitHub. |
Got it, thanks !
|
@@ -979,6 +979,20 @@ Lets you pass a command as a string to execute it as Django, like so:: | |||
|
|||
django-admin shell --command="import django; print(django.__version__)" | |||
|
|||
.. versionadded:: 1.11 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is necessarily a 'versionadded', more of a 'versionchanged' since you could already pass code in on stdin
print(django.__version__) | ||
EOF | ||
|
||
Note that Windows select() implementation doesn't support file descriptors, so |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since it's a function, select()
should be highlighted as such, i.e.
``select()``
.. versionadded:: 1.11 | ||
|
||
Lets you pass a script to execute via standard input on Linux and have it | ||
output to standard output, ie:: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Lets you pass a script" is a bit fluffy. Also you mean Unix or Posix, not just Linux 😉 Perhaps something like:
"You can now pass code in on standard input and it will be executed without any extra output from the REPL. For example::"
|
||
Note that Windows select() implementation doesn't support file descriptors, so | ||
the output on Windows hosts will contain additional, interface specific | ||
outputs, making output parsing a lot harder. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it should say something like
"On Windows the REPL is still outupt, due to limitations in its implementation of select()
."
Output parsing is a second-order effect and probably doesn't need to be mentioned directly
507c61e
to
a848ede
Compare
Thanks again for your dedication, you're setting the bar high for other repository maintainers, keep up the great work 🎸 |
merged in bf6392b, thanks! |
Thanks, I hope you enjoy this too one day because you deserved it ! |
Fun automation with standard input pipes::
https://code.djangoproject.com/ticket/27600