diff --git a/IPython/core/display.py b/IPython/core/display.py index bd098e7..851016f 100644 --- a/IPython/core/display.py +++ b/IPython/core/display.py @@ -6,6 +6,7 @@ from binascii import b2a_base64, hexlify +import html import json import mimetypes import os @@ -371,7 +372,7 @@ class DisplayObject(object): with gzip.open(BytesIO(data), 'rt', encoding=encoding) as fp: encoding = None data = fp.read() - + # decode data, if an encoding was specified # We only touch self.data once since # subclasses such as SVG have @data.setter methods @@ -802,7 +803,7 @@ class Image(DisplayObject): def __init__(self, data=None, url=None, filename=None, format=None, embed=None, width=None, height=None, retina=False, - unconfined=False, metadata=None): + unconfined=False, metadata=None, alt=None): """Create a PNG/JPEG/GIF image object given raw data. When this object is returned by an input cell or passed to the @@ -847,6 +848,8 @@ class Image(DisplayObject): Set unconfined=True to disable max-width confinement of the image. metadata : dict Specify extra metadata to attach to the image. + alt : unicode + Alternative text for the image, for use by screen readers. Examples -------- @@ -924,6 +927,7 @@ class Image(DisplayObject): self.height = height self.retina = retina self.unconfined = unconfined + self.alt = alt super(Image, self).__init__(data=data, url=url, filename=filename, metadata=metadata) @@ -933,6 +937,9 @@ class Image(DisplayObject): if self.height is None and self.metadata.get('height', {}): self.height = metadata['height'] + if self.alt is None and self.metadata.get('alt', {}): + self.alt = metadata['alt'] + if retina: self._retina_shape() @@ -962,18 +969,21 @@ class Image(DisplayObject): def _repr_html_(self): if not self.embed: - width = height = klass = '' + width = height = klass = alt = '' if self.width: width = ' width="%d"' % self.width if self.height: height = ' height="%d"' % self.height if self.unconfined: klass = ' class="unconfined"' - return u''.format( + if self.alt: + alt = ' alt="%s"' % html.escape(self.alt) + return u''.format( url=self.url, width=width, height=height, klass=klass, + alt=alt, ) def _repr_mimebundle_(self, include=None, exclude=None): @@ -1006,6 +1016,8 @@ class Image(DisplayObject): md['height'] = self.height if self.unconfined: md['unconfined'] = self.unconfined + if self.alt: + md['alt'] = self.alt if md or always_both: return b64_data, md else: diff --git a/IPython/core/tests/test_display.py b/IPython/core/tests/test_display.py index be0f85c..b7b4400 100644 --- a/IPython/core/tests/test_display.py +++ b/IPython/core/tests/test_display.py @@ -77,12 +77,12 @@ def test_embed_svg_url(): from io import BytesIO svg_data = b'' url = 'http://test.com/circle.svg' - + gzip_svg = BytesIO() with gzip.open(gzip_svg, 'wb') as fp: fp.write(svg_data) gzip_svg = gzip_svg.getvalue() - + def mocked_urlopen(*args, **kwargs): class MockResponse: def __init__(self, svg): @@ -94,7 +94,7 @@ def test_embed_svg_url(): if args[0] == url: return MockResponse(svg_data) - elif args[0] == url + 'z': + elif args[0] == url + 'z': ret= MockResponse(gzip_svg) ret.headers['content-encoding']= 'gzip' return ret @@ -105,7 +105,7 @@ def test_embed_svg_url(): nt.assert_true(svg._repr_svg_().startswith('' % (thisurl), img._repr_html_()) + img = display.Image(url=thisurl, unconfined=True, alt='an image') + nt.assert_equal(u'an image' % (thisurl), img._repr_html_()) + img = display.Image(url=thisurl, alt='>"& <') + nt.assert_equal(u'>"& <' % (thisurl), img._repr_html_()) + + img = display.Image(url=thisurl, metadata={'alt':'an image'}) + nt.assert_equal(img.alt, 'an image') + + here = os.path.dirname(__file__) + img = display.Image(os.path.join(here, "2x2.png"), alt='an image') + nt.assert_equal(img.alt, 'an image') + _, md = img._repr_png_() + nt.assert_equal(md['alt'], 'an image')