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
Add image message handler in ZMQTerminalInteractiveShell #1946
Conversation
I don't think we should always import PIL at startup - that will delay startup for something that's probably not needed in many cases. We could either disable it by default, so that PIL is loaded if a particular config option is set, or delay the import until the first time it receives an image. In terms of the API, checking against a list of strings seems awkward. I'd suggest simply setting |
@@ -21,13 +21,21 @@ | |||
import signal | |||
import sys | |||
import time | |||
from cStringIO import StringIO |
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.
For Python 3 compatibility, this should be from io import BytesIO
. io
is available in all the Python versions IPython supports.
Thanks for the review. Regarding the API, I think it's better to have some kind of fallback mechanism to help sharing config file in different machines. In that case, traits something like ListOfDottedObjectName is needed. But if this fallback style is not the IPython style, I will just change to DottedObjectName. I will apply other suggestions. |
That makes sense, but I'd want to hear more about possible use cases with a fallback system - I'm generally wary of building more complex architectures without a practical reason. Especially because handling lists in config ends up with this kind of awkward dance:
I'm sure others will chime in on this in a bit. If we do decide to go down that route, there's no real need for a |
Other handlers I have in mind are:
Come to think of it, I just remember that config is a python file so you can check if PIL is available in the config file. I guess there is no need for the fallback system. |
That's all reasonable, but the list could mean "try to do all of these", or "try to do each of these until one succeeds". It's also possible to use a single callable that tries multiple things. In fact, we already have a class for "try until one succeeds" - it's |
So, if user want to have fallback system s/he must use CommandChainDispatcher and the hypothetical "Python callback" handler. Is this what you mean? |
I'd thought also that having this would be very cool, but I think it has enough implementation fine points (PIL vs native platform tools, config, etc) that we should probably mark it for 0.14. @takluyver, thoughts?? |
Using a CCD as the handler, your config file would do something like:
I think I agree with @fperez that this is best done for 0.14. |
OK, tagging as such. |
I am fine with marking it as 0.14! @takluyver I thought I'd have c.ZMQTerminalInteractiveShell.image_handler = 'Callable'
c.ZMQTerminalInteractiveShell.callable_image_handler = CommandChainDispatcher([show_in_pil, save_to_file]) Other configuration example will be like: c.ZMQTerminalInteractiveShell.image_handler = 'TempFile'
c.ZMQTerminalInteractiveShell.tempfile_image_handler = ['my_viewer', '--file', '{0}'] # temp file goes into '{0}' What do you think? |
Why not just have the callable thing, and offer PIL, stream and tempfile as functions which it can be set to? That way we only need one configurable value, not two. That's still compatible with special casing some short names so they're quick to set - here's an example that takes two special cases or an importable name: |
How do you configure external program when you have only one configurable? Something like this? from IPython.somewhere import TempFile
c.ZMQTerminalInteractiveShell.image_handler = TempFile(['my_viewer', '--file', '{0}']) |
There's various ways you could set the arguments for the image viewer - a second config value may make sense there. I was more objecting to the split between |
Wait, putting a callable in image_handler is an error if the trait is DottedObjectName, right? How can you have a configuration that can be a string or a callable? |
Yeah, I've gone back on my idea about |
What trait I should use in that case? |
I think Any is fine - let's not overcomplicate this. I'd rather not |
I am awkward to use Any for configurable because I think whole point of using trait is to make things type safe... I'd like to stick to my Enum solution because it become little bit verbose only when using the callable and I think you'd accept little bit verbose config file when you configuring complex stuff like user defined callable. What do you think? |
I think 2 configurables is still more complexity than 1. Both in terms of code, and for the user - it might not be obvious that Remember that you can write a function to be called when the trait is changed, so you could validate the input there (if I understand traits correctly). Another way would be to create a new |
def handle_rich_data(self, data): | ||
for mime in ['image/png', 'image/jpeg', 'image/svg+xml']: | ||
if mime in data: | ||
self.handle_image(data, mime) |
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 should return after handling, in case of something that has both PNG/SVG reprs, you don't want to display both.
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.
Fixed. Thanks for the quick review!
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.
Don't you want to directly loop over _imagemime.keys()
?
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 list defines preference order. PNG is chose over JPEG and SVG, when they are sent together, for example. But I agree that it is confusing. It took me for a while to remember what is this. Probably I should use ordered dict? Although it is not in Python 2.6.
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.
Make sense, I should have thought of that. I just saw the same order on both, sorry.
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 moved it to a variable and made it configurable.
I just made a sample implementation using Enum. In terms of code, I think it is better to have homogeneous type to simplify code. If you allow callable to image_handler, you need to add more if block. For the user side, you already have different configurables for external program. Why not for callable? It's verbose, but symmetric. BTW, configurations I put does not show up by the command |
It needs to be added to the shell's configurables list to show up in %config. MagicsManager.register should be doing this. |
I implemented everything I want to add in this PR. I think the remaining work is config API tweaks. |
Now that the 0.13 is out, should I rebase it so that the review is easier? |
No need: as long as there are no conflicts (which in this case there aren't), it doesn't matter. Just note that we may be a bit slow with review for a bit, the release blitz put us all behind on other 'real life' things :) |
No problem. Thanks for the great release! |
I'll be in Sweden for the next week or so, and won't be doing much review between now and our SciPy tutorial Tuesday July 17, but then I should be doing lots of IPython work for the rest of that week while at SciPy. |
args = [s.format(**fmt) for s in self.stream_image_handler] | ||
proc = subprocess.Popen( | ||
args, stdin=subprocess.PIPE, | ||
stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
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.
Pedantic, but since we aren't reading stdout
or stderr
best practices would suggest redirecting them to /dev/null
. In Python >= 3.3 this would be as simple as stdout=subprocess.DEVNULL
, but instead the code gets a bit uglier:
with open(os.devnull, 'w') as devnull:
proc = subprocess.Popen(..., stdout=devnull, stderr=devnull)
proc.communicate(raw)
PIL.Image is actually a module.
assert False | ||
|
||
shell = self.shell | ||
shell.handle_image_PIL |
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.
Should these lines be like shell.handle_image_PIL = raise_if_called
?
Thanks, I'm more comfortable with this knowing that there are tests. Inevitably I had a few questions about the test code, but they should mostly be quick fixes (apart from maybe the 'does this work on Windows?' one). |
raise_if_called was not used as intended
Thanks for the comment. I fixed them. |
BTW, these are sample settings, in case you want to try quickly: PIL type handler: c.ZMQTerminalInteractiveShell.image_handler = 'PIL' Stream type handler: c.ZMQTerminalInteractiveShell.image_handler = 'stream'
c.ZMQTerminalInteractiveShell.stream_image_handler = ['display'] Tempfile type handler: c.ZMQTerminalInteractiveShell.image_handler = 'tempfile'
c.ZMQTerminalInteractiveShell.tempfile_image_handler = ['eog', '{file}'] Callable type handler: c.ZMQTerminalInteractiveShell.image_handler = 'callable'
c.ZMQTerminalInteractiveShell.callable_image_handler = some_callable I found stream+display most useful. |
I've tested stream and tempfile. One thought: execution is blocked until you close the image viewer. Would it make more sense to bring it up and immediately return to the prompt? In the tempfile case, I guess that means we'd have to leave the tempfile lying around, but I think /tmp is supposed to be cleaned with each restart anyway. I don't have strong views on this either way (I'm not a big user of |
Well, easiest solution is to use image viewer which exits after opening window. For example, you can use web browser for that: c.ZMQTerminalInteractiveShell.tempfile_image_handler = ['google-chrome', '{file}'] or Emacs: c.ZMQTerminalInteractiveShell.tempfile_image_handler = ['emacsclient', '--no-wait', '{file}'] I don't know if leaving many temporary files is ok. As you can workaround the problem by choosing image viewer, I think the current version is good for a start. I guess better solution is to create a temporary directory which persists until client ends its session. But it is out of this PR's scope. BTW, leaving process means leaving file object opening /dev/null around. Is it ok to do? |
OK, if that's going to get complicated, let's leave it for now. We can |
Not ready for pull yet? |
Sorry, I was away on a conference & holiday for the last week or so. I think it's OK, but I'd like someone else to have one more glance over it before we merge it. |
Looks ok to me. |
ping :) |
Thank for your patience @tkf I'm going to merge this one. |
Add image message handler in ZMQTerminalInteractiveShell This change introduces several handlers for messages which contain image in ZMQTerminalInteractiveShell. This is useful, for example, when connecting to the kernel in which pylab inline backend is activated. This PR will fix #1575.
Thank you very much! |
Add image message handler in ZMQTerminalInteractiveShell This change introduces several handlers for messages which contain image in ZMQTerminalInteractiveShell. This is useful, for example, when connecting to the kernel in which pylab inline backend is activated. This PR will fix ipython#1575.
This change introduces handler for messages which contain image in
ZMQTerminalInteractiveShell. This is useful, for example, when
connecting to the kernel in which pylab inline backend is activated.
Current image handler only supports PIL backend.
This PR will fix #1575.