-
Notifications
You must be signed in to change notification settings - Fork 945
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 filename and URL support for Image
#1676
Conversation
It seems the test failures are because I was assuming that I was under the impression that generally a tests directory is not included as part of the package (part of the source distribution, but not available from the library), is there a specific reason it was done this way? For the moment I guess I can add the image as part of the package_data, but it might be worth fooling around with the way tests are handled so that the actual installed package can be slimmer? |
I actually forgot during the sprint that I've done sth 'similar' with the (cc @jasongrout )
Although the |
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.
Look good! Maybe some rewording from filename -> file.
ipywidgets/widgets/widget_image.py
Outdated
@@ -32,3 +33,40 @@ class Image(DOMWidget, ValueWidget, CoreWidget): | |||
width = CUnicode(help="Width of the image in pixels.").tag(sync=True) | |||
height = CUnicode(help="Height of the image in pixels.").tag(sync=True) | |||
value = Bytes(help="The image data as a byte string.").tag(sync=True) | |||
|
|||
@classmethod | |||
def from_filename(cls, filename, **kwargs): |
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'd say from_file
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.
That's a reasonable change. The main reason I didn't go with from_file
was that it seemed like from_file
would take a stream buffer object (like an open file
), rather than a filename
.
I suppose I could change it so that it's from_file
and try to infer whether it's already an open file or a file path.
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.
You're right, that will be expected, and a nice feature :)
ipywidgets/widgets/widget_image.py
Outdated
@classmethod | ||
def from_filename(cls, filename, **kwargs): | ||
""" | ||
Create an `Image` from a local filename. |
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.
... from a local file.
?
var oldurl = this.el.src; | ||
this.el.src = url; | ||
if (oldurl) { | ||
if (oldurl && typeof oldurl !== 'string') { |
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.
quite tricky, but createObjectURL does create a string object. Maybe sth like:
if (oldurl && oldurl.startsWith('blob:')) {
...
}
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.
Hm... If it's just a string does it still need to be freed? Or is it just cast to a string when accessed through this.el.src
?
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.
It is a string of the form, 'blob:<someid>'
, see #1685 how I solved it for the videostream.
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.
Yeah, I played around with it in the console and I see what's going on. The src
attribute is a string that points to the location of the resource, which in this case is an in-memory blob.
I think the only downside of this startsWith('blob:')
approach is that a user might create another blob that they don't want revoked, and pass that one's resource locator to the model, and when they change the image, we'll be freeing their blob, not our blob. This seems like an extreme edge case, though, and I'm sure there are simple enough workarounds that dealing with this edge case is not worth adding additional complexity.
I imagine that the "right" way to do this (which avoids this edge case) would be to set a flag on the model that indicates whether the current URL was allocated by update
, and if so free it - rather than trying to infer this from the value at all.
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 users have access to the blob:...
strings, so that would indeed be rather unlikely.
The docs are a big vague, but it seems to be its reference counting behind the scenes, so I wouldn't worry about it.
I think it's better to include the tests and (provided the data for testing is small) to include this as well. In case of issues you can always ask users to run the tests. |
@maartenbreddels I'm not sure it particularly helps if the user has the tests in the module unless there's a specific mechanism for test discovery I don't know about. I think this is not common, but it's less work for me to do it that way so I guess I'm not complaining too much ;) |
@maartenbreddels, @pganssle - where are we on this? |
@pganssle - I think I resolved the conflict with master correctly, but it wouldn't hurt to look at it. |
As mentioned in #1685 it would be nice to have the API for Image and VideoStream with the static methods similar:
|
Those are convenience methods? That sounds all right to me. |
Last time I changed anything on this, I ended up spinning my wheels again trying to mess around with the tests. I guess I'll just ship the test data with the tests, but I still think it's not a good idea for the test directory itself to be importable from the package. I means you have to ship a bunch of unnecessary data with your package for something that's not useful for end-users anyway. I'll clean this up soon, and maybe make a separate PR for review that moves the tests outside of the package. |
Why do you think it is not a good idea? I haven't made up my mind yet
though. An advantage I see is that you can ask users to run the tests if
you ship it. What is the downside?
(from mobile phone)
…On 24 Oct 2017 17:16, "Paul Ganssle" ***@***.***> wrote:
Last time I changed anything on this, I ended up spinning my wheels again
trying to mess around with the tests. I guess I'll just ship the test data
with the tests, but I still think it's not a good idea for the test
directory itself to be importable from the package. I means you have to
ship a bunch of unnecessary data with your package for something that's not
useful for end-users anyway.
I'll clean this up soon, and maybe make a separate PR for review that
moves the tests outside of the package.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#1676 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/ABryPVacuv9wpFkLEzl2MQAQpVhFG_kgks5svf9XgaJpZM4PDnfB>
.
|
I think for one thing the abstraction is wrong - tests are not user-facing code, they are part of development and deployment. They don't serve to benefit the user at all, except indirectly in that it might aid the developers to diagnose or fix a problem. Some of the problems of shipping the tests are basically problems mainly because they are burdens on the user (albeit possibly minor) with no benefit for the user. Some downsides I see:
Most of these are not huge show-stoppers in most situations, but the benefit you get (users can run tests even from a wheel install) is not really worth it. As long as the tests are included in the source distribution, users who really want to run the tests can do so pretty easily, though I think this won't be something that is of general interest anyway, since most of the time a user has problems with something specific that your library is failing at (i.e. they already have a failing test case). Unit tests are more useful for saying, "is this library failing, and if so, how?" As long as the tests have already been run upstream, the only reason to run them again once you have them is to check that they work on your particular platform. That's something that is useful for people running package managers (e.g. debian, arch linux), but they're probably building from source or repo anyway. |
+1 for moving tests out of the distributed package - I think you've eloquently spoken of reasons for doing that. That change should be a different PR as you mention, of course. |
Thanks Paul, I've made up my mind I think. I'm currently splitting vaex up into multiple packages (but monorepo), and then it becomes a bit blurry which test belongs to which package, so it would be more natural to not include it in the source tree. For the testing framework, my vote goes to py.test, nosetests is maintained but dead, and I'm really positive about py.test (fixtures are great, capturing stdout keeps the noise down, just running failed tests, jumping to the debugger on fail). |
Ping on this. We thought it would be nice to go in to the 7.2 release, which we're hoping to do by early next week. |
let url; | ||
let format = this.model.get('format'); | ||
let value = this.model.get('value'); | ||
if (format != 'url') { |
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.
!==
please
var blob = new Blob([value], {type: `image/${this.model.get('format')}`}); | ||
url = URL.createObjectURL(blob); | ||
} else { | ||
url = String.fromCharCode.apply(null, new Uint8Array(value.buffer)); |
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.
Does this work for utf8 urls? See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/fromCharCode#Getting_it_to_work_with_higher_values.
It's probably better to use the TextDecoder api (and shim for IE?): https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder
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.
In fact, apparently the notebook itself uses TextDecoder to get the JSON parts of a binary message: https://github.com/jupyter/notebook/blob/179bb24fbf79d153812858126127a91431da3319/notebook/static/services/kernels/serialize.js#L20
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.
Weird that it is used in the notebook, especially since it is not supported in IE 11 and in development in Edge: https://developer.microsoft.com/en-us/microsoft-edge/platform/status/encodingstandard/
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.
Looks like the notebook already includes a polyfill: jupyter/notebook#3457
I think we should assume the TextDecoder api works on the page. The notebook has a polyfill, and lab doesn't support IE/Edge. People embedding widgets, if the widgets use binary messages, will need to put it on the page.
@jasongrout Sorry this has totally fallen off my radar. Depending on how early my son falls asleep, I'll either make these changes tonight, tomorrow night or this weekend. Because I don't use any sort of task management software or anything, I'm going to leave the tab open, nagging at me until I get to it 😛 |
At this point, I consider open tabs as my task management system 😅 |
Image
Image
This looks good to me. I'll leave it open for the European working hours for any comments from @SylvainCorlay or @maartenbreddels. If there is no objection, I'll merge. Thanks @pganssle! |
ipywidgets/widgets/widget_image.py
Outdated
else: | ||
return str | ||
|
||
_text_type = _text_type() |
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'd say a more pythonic way would be
_text_type = str
try:
_text_type = unicode
except:
pass
url: [str, bytes] | ||
The location of a URL to load. | ||
""" | ||
if isinstance(url, _text_type): |
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 would fail for a str type in Python 2, you could do (avoiding six, and what is done in widget.py):
from ipython_genutils.py3compat import string_types
if isinstance(url, string_types):
....
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.
It's not intended to match str
, it's intended to match unicode
. str
in Python 2 does not need to be decoded.
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.
Ok, makes sense (maybe a comment?)
Returns an `Image` with the value set from the filename. | ||
""" | ||
value = cls._load_file_value(filename) | ||
|
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.
What about guessing format:
base, ext = os.path.splitext(filename)
if ext and 'format' not in kwargs:
kwargs = dict(**kwargs, format=ext[1:]) # copy dict and remove leading .
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.
@maartenbreddels Good idea, though I think the implementation needs to be a bit more complicated - it should try to map the extension to the MIME type, which is not always the same thing.
I'll fix that up.
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.
Yes, we can choose maybe from a whitelist (all the ones starting with image/). Maybe give a warning if it's not known?
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.
You could just use the python library for mapping filenames to formats: https://docs.python.org/3/library/mimetypes.html#mimetypes.guess_type
|
||
@classmethod | ||
def _load_file_value(cls, filename): | ||
if getattr(filename, 'read', None) is not None: |
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.
Any reason not to use if hasattr(filename, 'read')
?
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.
Yes. It is mainly because it's dangerous in Python 2.
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.
Thanks, always learning!
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.
Hi Paul,
thanks a lot, good work, I got a few comments, feel free to disagree!
cheers,
Maarten
@maartenbreddels OK, added in the MIME type inference. |
name = getattr(filename, 'name', None) | ||
name = name or filename | ||
|
||
try: |
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.
Can we use the python mimetypes library? That seems cleaner...
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.
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.
@jasongrout Good call, didn't know about that.
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.
Updated. I think this is much cleaner.
This is a temporary measure until the tests can be removed from the final package entirely and left only in the repository.
@classmethod | ||
def _guess_format(cls, filename): | ||
# file objects may have a .name parameter | ||
name = getattr(filename, 'name', None) |
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.
Didn't know that!
@maartenbreddels - feel free to merge when this looks good to you! |
Excellent work Paul! # I is numpy arrays with 'intensities' of shape (height, width)
colormap = matplotlib.cm.get_cmap(colormap)
rgba = colormap(I, bytes=True)
width, height = rgba.shape[1], rgba.shape[0]
f = StringIO()
img = PIL.Image.frombuffer("RGBA", (width, height), rgba, 'raw', 'RGBA', 0, 0)
img.save(f, "png")
return f.getvalue() Do we want?
I think we can add support for this without explicitly importing PIL/Pillow, and happy to do a separate PR for this is Paul feels like it enough :) |
I think definitely separate PR - this one has evolved enough. |
@maartenbreddels Probably best not to let the best be the enemy of the good. It's always possible to add features in later PRs or later releases. One thing to note is that if you are going to have a lot of variant forms of roughly the same thing, for explicit dispatch I have written a library called variants that provides some syntactic sugar for adding variant methods. Unfortunately it does not currently work with classmethods, but that's definitely coming (plus there are other ways to implement the same general concept without the variants library that will work with classmethods). Here are the slides for a lightning talk I gave about this, if you are interested. |
@pganssle you seemed to be on roll, didn't want to hold you back if you had energy left ;) |
Can someone tell how to add hyperlink on image in ipywidgets. I tried the below syntax. This works perfectly on jupyter notebooks cell output. But on Voila, image is not getting displayed on Voila
|
Fixes #435.
This adds two convenience methods for pulling images directly from filenames (one alternate constructor and one "setter"-style method). It also adds support for the "url" format (per @SylvainCorlay's advice), which interprets the
value
as a URL.I would also like to automatically detect when
value
has been set to asix.text_type
(str
/unicode
) and automatically setformat
to'url'
, but apparently validators fire after the type checking. Is there a hook in traitlets that fires before type checking?I added some very basic tests, but I was not entirely sure how to mock the front-end and capture the messages - if someone can point me in the right direction I can improve these tests. I did not add any tests on the JS side - any advice on how to write tests for the front end would be appreciated.