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 #27600 -- Improved the way the 'shell' command reads python code from stdin. #7681

Closed
wants to merge 9 commits into from

Conversation

jpic
Copy link
Contributor

@jpic jpic commented Dec 13, 2016

Fun automation with standard input pipes::

$ ./manage.py shell <<EOF
> import pprint
> import json
> pprint.pprint(json.dumps('fun'))
> EOF
 'fun'

https://code.djangoproject.com/ticket/27600

@timgraham
Copy link
Member

timgraham commented Dec 13, 2016

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

@jpic
Copy link
Contributor Author

jpic commented Dec 14, 2016

Well it's not the same, this sorts of works:

$ manage.py shell <<EOF
> print 'hi'
> 
> EOF
Python 2.7.12 (default, Nov  7 2016, 11:55:55) 
Type "copyright", "credits" or "license" for more information.

IPython 2.3.1 -- An enhanced Interactive Python.
?         -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
help      -> Python's own help system.
object?   -> Details about 'object', use 'object??' for extra details.

��In [��1��]: ��hi

��In [��2��]: ��
��In [��2��]: ��
Do you really want to exit ([y]/n)? 

That's better:

$ manage.py shell <<EOF
print 'hi'
EOF
hi

Thanks !

@jpic
Copy link
Contributor Author

jpic commented Dec 14, 2016

I removed the requirement to pass -c - to just mimic the behaviour of the python executable: it'll execute what it reads from stdin instead of opening an interactive shell if it finds anything was sent to stdin.

Is it ok if I wait until the ticket is accepted to write documentation and a test ?

@timgraham timgraham changed the title Read from stdin for shell -c - Fixed #27600 -- Allowed the shell command to read from stdin. Dec 14, 2016
Copy link
Sponsor Member

@adamchainz adamchainz left a 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]:
Copy link
Sponsor Member

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.

Copy link
Contributor Author

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
Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment before the if?

@adamchainz
Copy link
Sponsor Member

I imagine it could be useful in a shell script to be able to do ./manage.py shell <<< 'from models import Customer; Customer.objects.count()'

@adamchainz adamchainz changed the title Fixed #27600 -- Allowed the shell command to read from stdin. Fixed #27600 -- Improved the way the 'shell' command reads from stdin. Dec 15, 2016
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."
Copy link
Sponsor Member

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')
Copy link
Sponsor Member

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

Copy link
Contributor Author

@jpic jpic Dec 16, 2016

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.

Copy link
Sponsor Member

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

Copy link
Contributor Author

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 🎸

@jpic
Copy link
Contributor Author

jpic commented Dec 15, 2016

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 ?

@jpic
Copy link
Contributor Author

jpic commented Dec 16, 2016

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 exec(sys.stdin.read) call so it has absolutely no effect, and needs to be mocked. Basically when trying:

    stdout = StringIO()
    stdin.read.return_value = 'print(100)'
    call_command(
        'shell',
        stdout=stdout,
    )

stdout will be empty, because exec(sys.stdin.read()) doesn't use the OutputWrapper that was passed. Same goes for stdin.

select() was mocked because select(...)[0] has to return something true-ish to enter the if block, and that can otherwise only be possible if there's really something to read in sys.stdin. I guess that's exactly why the test passes on windows, even though it would never work on windows as stated by python select's documentation note:

Note File objects on Windows are not acceptable, but sockets are. On Windows, the underlying select() function is provided by the WinSock library, and does not handle file descriptors that don’t originate from WinSock.

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."
Copy link
Sponsor Member

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',
)
Copy link
Sponsor Member

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]:
Copy link
Sponsor Member

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'

@adamchainz
Copy link
Sponsor Member

LGTM apart from a release note in docs/releases/1.11.txt

@adamchainz adamchainz changed the title Fixed #27600 -- Improved the way the 'shell' command reads from stdin. Fixed #27600 -- Improved the way the 'shell' command reads python code from stdin. Dec 20, 2016
@jpic
Copy link
Contributor Author

jpic commented Dec 20, 2016

@adamchainz thanks a heap mate, you 🎸

@timgraham
Copy link
Member

Tested on Windows using Git bash:

$ python manage.py shell << EOF
> print('foo')
> EOF
...
  File "c:\users\tim\code\django\django\core\management\commands\shell.py", line 124, in handle
    if select.select([sys.stdin], [], [], 0)[0]:
OSError: [WinError 10038] An operation was attempted on something that is not a socket

@@ -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"
Copy link
Member

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
Copy link
Member

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.

Copy link
Sponsor Member

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)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

chop blank line?

@jpic
Copy link
Contributor Author

jpic commented Dec 20, 2016

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 ?

@timgraham
Copy link
Member

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.

@adamchainz
Copy link
Sponsor Member

Can't you detect windows and not even try select.select in that case? Then we'll get the old behaviour on windows which should be fine for the time being.

@jpic
Copy link
Contributor Author

jpic commented Dec 20, 2016

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().")
Copy link
Sponsor Member

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

@timgraham
Copy link
Member

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.

@jpic
Copy link
Contributor Author

jpic commented Dec 20, 2016 via email

@timgraham
Copy link
Member

The workflow I use is to address all review comments locally, then push to GitHub.

@jpic
Copy link
Contributor Author

jpic commented Dec 20, 2016 via email

@@ -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
Copy link
Sponsor Member

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
Copy link
Sponsor Member

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::
Copy link
Sponsor Member

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.
Copy link
Sponsor Member

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

@jpic jpic force-pushed the shell_stdin branch 2 times, most recently from 507c61e to a848ede Compare December 21, 2016 12:19
@jpic
Copy link
Contributor Author

jpic commented Dec 21, 2016

Thanks again for your dedication, you're setting the bar high for other repository maintainers, keep up the great work

🎸

@timgraham
Copy link
Member

merged in bf6392b, thanks!

@timgraham timgraham closed this Dec 21, 2016
@jpic
Copy link
Contributor Author

jpic commented Dec 22, 2016

Thanks, I hope you enjoy this too one day because you deserved it !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants