##// END OF EJS Templates
Add normalize parameter to Audio.
Matan Gover -
Show More
@@ -1,617 +1,634 b''
1 """Various display related classes.
1 """Various display related classes.
2
2
3 Authors : MinRK, gregcaporaso, dannystaple
3 Authors : MinRK, gregcaporaso, dannystaple
4 """
4 """
5 from html import escape as html_escape
5 from html import escape as html_escape
6 from os.path import exists, isfile, splitext, abspath, join, isdir
6 from os.path import exists, isfile, splitext, abspath, join, isdir
7 from os import walk, sep, fsdecode
7 from os import walk, sep, fsdecode
8
8
9 from IPython.core.display import DisplayObject, TextDisplayObject
9 from IPython.core.display import DisplayObject, TextDisplayObject
10
10
11 __all__ = ['Audio', 'IFrame', 'YouTubeVideo', 'VimeoVideo', 'ScribdDocument',
11 __all__ = ['Audio', 'IFrame', 'YouTubeVideo', 'VimeoVideo', 'ScribdDocument',
12 'FileLink', 'FileLinks', 'Code']
12 'FileLink', 'FileLinks', 'Code']
13
13
14
14
15 class Audio(DisplayObject):
15 class Audio(DisplayObject):
16 """Create an audio object.
16 """Create an audio object.
17
17
18 When this object is returned by an input cell or passed to the
18 When this object is returned by an input cell or passed to the
19 display function, it will result in Audio controls being displayed
19 display function, it will result in Audio controls being displayed
20 in the frontend (only works in the notebook).
20 in the frontend (only works in the notebook).
21
21
22 Parameters
22 Parameters
23 ----------
23 ----------
24 data : numpy array, list, unicode, str or bytes
24 data : numpy array, list, unicode, str or bytes
25 Can be one of
25 Can be one of
26
26
27 * Numpy 1d array containing the desired waveform (mono)
27 * Numpy 1d array containing the desired waveform (mono)
28 * Numpy 2d array containing waveforms for each channel.
28 * Numpy 2d array containing waveforms for each channel.
29 Shape=(NCHAN, NSAMPLES). For the standard channel order, see
29 Shape=(NCHAN, NSAMPLES). For the standard channel order, see
30 http://msdn.microsoft.com/en-us/library/windows/hardware/dn653308(v=vs.85).aspx
30 http://msdn.microsoft.com/en-us/library/windows/hardware/dn653308(v=vs.85).aspx
31 * List of float or integer representing the waveform (mono)
31 * List of float or integer representing the waveform (mono)
32 * String containing the filename
32 * String containing the filename
33 * Bytestring containing raw PCM data or
33 * Bytestring containing raw PCM data or
34 * URL pointing to a file on the web.
34 * URL pointing to a file on the web.
35
35
36 If the array option is used, the waveform will be normalized.
36 If the array option is used, the waveform will be normalized.
37
37
38 If a filename or url is used, the format support will be browser
38 If a filename or url is used, the format support will be browser
39 dependent.
39 dependent.
40 url : unicode
40 url : unicode
41 A URL to download the data from.
41 A URL to download the data from.
42 filename : unicode
42 filename : unicode
43 Path to a local file to load the data from.
43 Path to a local file to load the data from.
44 embed : boolean
44 embed : boolean
45 Should the audio data be embedded using a data URI (True) or should
45 Should the audio data be embedded using a data URI (True) or should
46 the original source be referenced. Set this to True if you want the
46 the original source be referenced. Set this to True if you want the
47 audio to playable later with no internet connection in the notebook.
47 audio to playable later with no internet connection in the notebook.
48
48
49 Default is `True`, unless the keyword argument `url` is set, then
49 Default is `True`, unless the keyword argument `url` is set, then
50 default value is `False`.
50 default value is `False`.
51 rate : integer
51 rate : integer
52 The sampling rate of the raw data.
52 The sampling rate of the raw data.
53 Only required when data parameter is being used as an array
53 Only required when data parameter is being used as an array
54 autoplay : bool
54 autoplay : bool
55 Set to True if the audio should immediately start playing.
55 Set to True if the audio should immediately start playing.
56 Default is `False`.
56 Default is `False`.
57 normalize : bool
58 Whether audio should be normalized (rescaled) to the maximum possible
59 range. Default is `True`. When set to `False`, `data` must be between
60 -1 and 1 (inclusive), otherwise an error is raised.
61 Applies only when `data` is a list or array of samples; other types of
62 audio are never normalized.
57
63
58 Examples
64 Examples
59 --------
65 --------
60 ::
66 ::
61
67
62 # Generate a sound
68 # Generate a sound
63 import numpy as np
69 import numpy as np
64 framerate = 44100
70 framerate = 44100
65 t = np.linspace(0,5,framerate*5)
71 t = np.linspace(0,5,framerate*5)
66 data = np.sin(2*np.pi*220*t) + np.sin(2*np.pi*224*t)
72 data = np.sin(2*np.pi*220*t) + np.sin(2*np.pi*224*t)
67 Audio(data,rate=framerate)
73 Audio(data,rate=framerate)
68
74
69 # Can also do stereo or more channels
75 # Can also do stereo or more channels
70 dataleft = np.sin(2*np.pi*220*t)
76 dataleft = np.sin(2*np.pi*220*t)
71 dataright = np.sin(2*np.pi*224*t)
77 dataright = np.sin(2*np.pi*224*t)
72 Audio([dataleft, dataright],rate=framerate)
78 Audio([dataleft, dataright],rate=framerate)
73
79
74 Audio("http://www.nch.com.au/acm/8k16bitpcm.wav") # From URL
80 Audio("http://www.nch.com.au/acm/8k16bitpcm.wav") # From URL
75 Audio(url="http://www.w3schools.com/html/horse.ogg")
81 Audio(url="http://www.w3schools.com/html/horse.ogg")
76
82
77 Audio('/path/to/sound.wav') # From file
83 Audio('/path/to/sound.wav') # From file
78 Audio(filename='/path/to/sound.ogg')
84 Audio(filename='/path/to/sound.ogg')
79
85
80 Audio(b'RAW_WAV_DATA..) # From bytes
86 Audio(b'RAW_WAV_DATA..) # From bytes
81 Audio(data=b'RAW_WAV_DATA..)
87 Audio(data=b'RAW_WAV_DATA..)
82
88
83 """
89 """
84 _read_flags = 'rb'
90 _read_flags = 'rb'
85
91
86 def __init__(self, data=None, filename=None, url=None, embed=None, rate=None, autoplay=False):
92 def __init__(self, data=None, filename=None, url=None, embed=None, rate=None, autoplay=False, normalize=True):
87 if filename is None and url is None and data is None:
93 if filename is None and url is None and data is None:
88 raise ValueError("No audio data found. Expecting filename, url, or data.")
94 raise ValueError("No audio data found. Expecting filename, url, or data.")
89 if embed is False and url is None:
95 if embed is False and url is None:
90 raise ValueError("No url found. Expecting url when embed=False")
96 raise ValueError("No url found. Expecting url when embed=False")
91
97
92 if url is not None and embed is not True:
98 if url is not None and embed is not True:
93 self.embed = False
99 self.embed = False
94 else:
100 else:
95 self.embed = True
101 self.embed = True
96 self.autoplay = autoplay
102 self.autoplay = autoplay
97 super(Audio, self).__init__(data=data, url=url, filename=filename)
103 super(Audio, self).__init__(data=data, url=url, filename=filename)
98
104
99 if self.data is not None and not isinstance(self.data, bytes):
105 if self.data is not None and not isinstance(self.data, bytes):
100 if rate is None:
106 if rate is None:
101 raise ValueError("rate must be specified when data is a numpy array or list of audio samples.")
107 raise ValueError("rate must be specified when data is a numpy array or list of audio samples.")
102 self.data = Audio._make_wav(data, rate)
108 self.data = Audio._make_wav(data, rate, normalize)
103
109
104 def reload(self):
110 def reload(self):
105 """Reload the raw data from file or URL."""
111 """Reload the raw data from file or URL."""
106 import mimetypes
112 import mimetypes
107 if self.embed:
113 if self.embed:
108 super(Audio, self).reload()
114 super(Audio, self).reload()
109
115
110 if self.filename is not None:
116 if self.filename is not None:
111 self.mimetype = mimetypes.guess_type(self.filename)[0]
117 self.mimetype = mimetypes.guess_type(self.filename)[0]
112 elif self.url is not None:
118 elif self.url is not None:
113 self.mimetype = mimetypes.guess_type(self.url)[0]
119 self.mimetype = mimetypes.guess_type(self.url)[0]
114 else:
120 else:
115 self.mimetype = "audio/wav"
121 self.mimetype = "audio/wav"
116
122
117 @staticmethod
123 @staticmethod
118 def _make_wav(data, rate):
124 def _make_wav(data, rate, normalize):
119 """ Transform a numpy array to a PCM bytestring """
125 """ Transform a numpy array to a PCM bytestring """
120 import struct
126 import struct
121 from io import BytesIO
127 from io import BytesIO
122 import wave
128 import wave
123
129
124 try:
130 try:
125 scaled, nchan = Audio._validate_and_normalize_with_numpy(data)
131 scaled, nchan = Audio._validate_and_normalize_with_numpy(data, normalize)
126 except ImportError:
132 except ImportError:
127 scaled, nchan = Audio._validate_and_normalize_without_numpy(data)
133 scaled, nchan = Audio._validate_and_normalize_without_numpy(data, normalize)
128
134
129 fp = BytesIO()
135 fp = BytesIO()
130 waveobj = wave.open(fp,mode='wb')
136 waveobj = wave.open(fp,mode='wb')
131 waveobj.setnchannels(nchan)
137 waveobj.setnchannels(nchan)
132 waveobj.setframerate(rate)
138 waveobj.setframerate(rate)
133 waveobj.setsampwidth(2)
139 waveobj.setsampwidth(2)
134 waveobj.setcomptype('NONE','NONE')
140 waveobj.setcomptype('NONE','NONE')
135 waveobj.writeframes(b''.join([struct.pack('<h',x) for x in scaled]))
141 waveobj.writeframes(b''.join([struct.pack('<h',x) for x in scaled]))
136 val = fp.getvalue()
142 val = fp.getvalue()
137 waveobj.close()
143 waveobj.close()
138
144
139 return val
145 return val
140
146
141 @staticmethod
147 @staticmethod
142 def _validate_and_normalize_with_numpy(data):
148 def _validate_and_normalize_with_numpy(data, normalize):
143 import numpy as np
149 import numpy as np
144
150
145 data = np.array(data, dtype=float)
151 data = np.array(data, dtype=float)
146 if len(data.shape) == 1:
152 if len(data.shape) == 1:
147 nchan = 1
153 nchan = 1
148 elif len(data.shape) == 2:
154 elif len(data.shape) == 2:
149 # In wave files,channels are interleaved. E.g.,
155 # In wave files,channels are interleaved. E.g.,
150 # "L1R1L2R2..." for stereo. See
156 # "L1R1L2R2..." for stereo. See
151 # http://msdn.microsoft.com/en-us/library/windows/hardware/dn653308(v=vs.85).aspx
157 # http://msdn.microsoft.com/en-us/library/windows/hardware/dn653308(v=vs.85).aspx
152 # for channel ordering
158 # for channel ordering
153 nchan = data.shape[0]
159 nchan = data.shape[0]
154 data = data.T.ravel()
160 data = data.T.ravel()
155 else:
161 else:
156 raise ValueError('Array audio input must be a 1D or 2D array')
162 raise ValueError('Array audio input must be a 1D or 2D array')
157 scaled = np.int16(data/np.max(np.abs(data))*32767).tolist()
163
164 max_abs_value = np.max(np.abs(data))
165 normalization_factor = Audio._get_normalization_factor(max_abs_value, normalize)
166 scaled = np.int16(data / normalization_factor * 32767).tolist()
158 return scaled, nchan
167 return scaled, nchan
159
168
169
160 @staticmethod
170 @staticmethod
161 def _validate_and_normalize_without_numpy(data):
171 def _validate_and_normalize_without_numpy(data, normalize):
162 try:
172 try:
163 maxabsvalue = float(max([abs(x) for x in data]))
173 max_abs_value = float(max([abs(x) for x in data]))
164 except TypeError:
174 except TypeError:
165 raise TypeError('Only lists of mono audio are '
175 raise TypeError('Only lists of mono audio are '
166 'supported if numpy is not installed')
176 'supported if numpy is not installed')
167
177
168 scaled = [int(x/maxabsvalue*32767) for x in data]
178 normalization_factor = Audio._get_normalization_factor(max_abs_value, normalize)
179 scaled = [int(x / normalization_factor * 32767) for x in data]
169 nchan = 1
180 nchan = 1
170 return scaled, nchan
181 return scaled, nchan
171
182
183 @staticmethod
184 def _get_normalization_factor(max_abs_value, normalize):
185 if not normalize and max_abs_value > 1:
186 raise ValueError('Audio data must be between -1 and 1 when normalize=False.')
187 return max_abs_value if normalize else 1
188
172 def _data_and_metadata(self):
189 def _data_and_metadata(self):
173 """shortcut for returning metadata with url information, if defined"""
190 """shortcut for returning metadata with url information, if defined"""
174 md = {}
191 md = {}
175 if self.url:
192 if self.url:
176 md['url'] = self.url
193 md['url'] = self.url
177 if md:
194 if md:
178 return self.data, md
195 return self.data, md
179 else:
196 else:
180 return self.data
197 return self.data
181
198
182 def _repr_html_(self):
199 def _repr_html_(self):
183 src = """
200 src = """
184 <audio controls="controls" {autoplay}>
201 <audio controls="controls" {autoplay}>
185 <source src="{src}" type="{type}" />
202 <source src="{src}" type="{type}" />
186 Your browser does not support the audio element.
203 Your browser does not support the audio element.
187 </audio>
204 </audio>
188 """
205 """
189 return src.format(src=self.src_attr(),type=self.mimetype, autoplay=self.autoplay_attr())
206 return src.format(src=self.src_attr(),type=self.mimetype, autoplay=self.autoplay_attr())
190
207
191 def src_attr(self):
208 def src_attr(self):
192 import base64
209 import base64
193 if self.embed and (self.data is not None):
210 if self.embed and (self.data is not None):
194 data = base64=base64.b64encode(self.data).decode('ascii')
211 data = base64=base64.b64encode(self.data).decode('ascii')
195 return """data:{type};base64,{base64}""".format(type=self.mimetype,
212 return """data:{type};base64,{base64}""".format(type=self.mimetype,
196 base64=data)
213 base64=data)
197 elif self.url is not None:
214 elif self.url is not None:
198 return self.url
215 return self.url
199 else:
216 else:
200 return ""
217 return ""
201
218
202 def autoplay_attr(self):
219 def autoplay_attr(self):
203 if(self.autoplay):
220 if(self.autoplay):
204 return 'autoplay="autoplay"'
221 return 'autoplay="autoplay"'
205 else:
222 else:
206 return ''
223 return ''
207
224
208 class IFrame(object):
225 class IFrame(object):
209 """
226 """
210 Generic class to embed an iframe in an IPython notebook
227 Generic class to embed an iframe in an IPython notebook
211 """
228 """
212
229
213 iframe = """
230 iframe = """
214 <iframe
231 <iframe
215 width="{width}"
232 width="{width}"
216 height="{height}"
233 height="{height}"
217 src="{src}{params}"
234 src="{src}{params}"
218 frameborder="0"
235 frameborder="0"
219 allowfullscreen
236 allowfullscreen
220 ></iframe>
237 ></iframe>
221 """
238 """
222
239
223 def __init__(self, src, width, height, **kwargs):
240 def __init__(self, src, width, height, **kwargs):
224 self.src = src
241 self.src = src
225 self.width = width
242 self.width = width
226 self.height = height
243 self.height = height
227 self.params = kwargs
244 self.params = kwargs
228
245
229 def _repr_html_(self):
246 def _repr_html_(self):
230 """return the embed iframe"""
247 """return the embed iframe"""
231 if self.params:
248 if self.params:
232 try:
249 try:
233 from urllib.parse import urlencode # Py 3
250 from urllib.parse import urlencode # Py 3
234 except ImportError:
251 except ImportError:
235 from urllib import urlencode
252 from urllib import urlencode
236 params = "?" + urlencode(self.params)
253 params = "?" + urlencode(self.params)
237 else:
254 else:
238 params = ""
255 params = ""
239 return self.iframe.format(src=self.src,
256 return self.iframe.format(src=self.src,
240 width=self.width,
257 width=self.width,
241 height=self.height,
258 height=self.height,
242 params=params)
259 params=params)
243
260
244 class YouTubeVideo(IFrame):
261 class YouTubeVideo(IFrame):
245 """Class for embedding a YouTube Video in an IPython session, based on its video id.
262 """Class for embedding a YouTube Video in an IPython session, based on its video id.
246
263
247 e.g. to embed the video from https://www.youtube.com/watch?v=foo , you would
264 e.g. to embed the video from https://www.youtube.com/watch?v=foo , you would
248 do::
265 do::
249
266
250 vid = YouTubeVideo("foo")
267 vid = YouTubeVideo("foo")
251 display(vid)
268 display(vid)
252
269
253 To start from 30 seconds::
270 To start from 30 seconds::
254
271
255 vid = YouTubeVideo("abc", start=30)
272 vid = YouTubeVideo("abc", start=30)
256 display(vid)
273 display(vid)
257
274
258 To calculate seconds from time as hours, minutes, seconds use
275 To calculate seconds from time as hours, minutes, seconds use
259 :class:`datetime.timedelta`::
276 :class:`datetime.timedelta`::
260
277
261 start=int(timedelta(hours=1, minutes=46, seconds=40).total_seconds())
278 start=int(timedelta(hours=1, minutes=46, seconds=40).total_seconds())
262
279
263 Other parameters can be provided as documented at
280 Other parameters can be provided as documented at
264 https://developers.google.com/youtube/player_parameters#Parameters
281 https://developers.google.com/youtube/player_parameters#Parameters
265
282
266 When converting the notebook using nbconvert, a jpeg representation of the video
283 When converting the notebook using nbconvert, a jpeg representation of the video
267 will be inserted in the document.
284 will be inserted in the document.
268 """
285 """
269
286
270 def __init__(self, id, width=400, height=300, **kwargs):
287 def __init__(self, id, width=400, height=300, **kwargs):
271 self.id=id
288 self.id=id
272 src = "https://www.youtube.com/embed/{0}".format(id)
289 src = "https://www.youtube.com/embed/{0}".format(id)
273 super(YouTubeVideo, self).__init__(src, width, height, **kwargs)
290 super(YouTubeVideo, self).__init__(src, width, height, **kwargs)
274
291
275 def _repr_jpeg_(self):
292 def _repr_jpeg_(self):
276 # Deferred import
293 # Deferred import
277 from urllib.request import urlopen
294 from urllib.request import urlopen
278
295
279 try:
296 try:
280 return urlopen("https://img.youtube.com/vi/{id}/hqdefault.jpg".format(id=self.id)).read()
297 return urlopen("https://img.youtube.com/vi/{id}/hqdefault.jpg".format(id=self.id)).read()
281 except IOError:
298 except IOError:
282 return None
299 return None
283
300
284 class VimeoVideo(IFrame):
301 class VimeoVideo(IFrame):
285 """
302 """
286 Class for embedding a Vimeo video in an IPython session, based on its video id.
303 Class for embedding a Vimeo video in an IPython session, based on its video id.
287 """
304 """
288
305
289 def __init__(self, id, width=400, height=300, **kwargs):
306 def __init__(self, id, width=400, height=300, **kwargs):
290 src="https://player.vimeo.com/video/{0}".format(id)
307 src="https://player.vimeo.com/video/{0}".format(id)
291 super(VimeoVideo, self).__init__(src, width, height, **kwargs)
308 super(VimeoVideo, self).__init__(src, width, height, **kwargs)
292
309
293 class ScribdDocument(IFrame):
310 class ScribdDocument(IFrame):
294 """
311 """
295 Class for embedding a Scribd document in an IPython session
312 Class for embedding a Scribd document in an IPython session
296
313
297 Use the start_page params to specify a starting point in the document
314 Use the start_page params to specify a starting point in the document
298 Use the view_mode params to specify display type one off scroll | slideshow | book
315 Use the view_mode params to specify display type one off scroll | slideshow | book
299
316
300 e.g to Display Wes' foundational paper about PANDAS in book mode from page 3
317 e.g to Display Wes' foundational paper about PANDAS in book mode from page 3
301
318
302 ScribdDocument(71048089, width=800, height=400, start_page=3, view_mode="book")
319 ScribdDocument(71048089, width=800, height=400, start_page=3, view_mode="book")
303 """
320 """
304
321
305 def __init__(self, id, width=400, height=300, **kwargs):
322 def __init__(self, id, width=400, height=300, **kwargs):
306 src="https://www.scribd.com/embeds/{0}/content".format(id)
323 src="https://www.scribd.com/embeds/{0}/content".format(id)
307 super(ScribdDocument, self).__init__(src, width, height, **kwargs)
324 super(ScribdDocument, self).__init__(src, width, height, **kwargs)
308
325
309 class FileLink(object):
326 class FileLink(object):
310 """Class for embedding a local file link in an IPython session, based on path
327 """Class for embedding a local file link in an IPython session, based on path
311
328
312 e.g. to embed a link that was generated in the IPython notebook as my/data.txt
329 e.g. to embed a link that was generated in the IPython notebook as my/data.txt
313
330
314 you would do::
331 you would do::
315
332
316 local_file = FileLink("my/data.txt")
333 local_file = FileLink("my/data.txt")
317 display(local_file)
334 display(local_file)
318
335
319 or in the HTML notebook, just::
336 or in the HTML notebook, just::
320
337
321 FileLink("my/data.txt")
338 FileLink("my/data.txt")
322 """
339 """
323
340
324 html_link_str = "<a href='%s' target='_blank'>%s</a>"
341 html_link_str = "<a href='%s' target='_blank'>%s</a>"
325
342
326 def __init__(self,
343 def __init__(self,
327 path,
344 path,
328 url_prefix='',
345 url_prefix='',
329 result_html_prefix='',
346 result_html_prefix='',
330 result_html_suffix='<br>'):
347 result_html_suffix='<br>'):
331 """
348 """
332 Parameters
349 Parameters
333 ----------
350 ----------
334 path : str
351 path : str
335 path to the file or directory that should be formatted
352 path to the file or directory that should be formatted
336 url_prefix : str
353 url_prefix : str
337 prefix to be prepended to all files to form a working link [default:
354 prefix to be prepended to all files to form a working link [default:
338 '']
355 '']
339 result_html_prefix : str
356 result_html_prefix : str
340 text to append to beginning to link [default: '']
357 text to append to beginning to link [default: '']
341 result_html_suffix : str
358 result_html_suffix : str
342 text to append at the end of link [default: '<br>']
359 text to append at the end of link [default: '<br>']
343 """
360 """
344 if isdir(path):
361 if isdir(path):
345 raise ValueError("Cannot display a directory using FileLink. "
362 raise ValueError("Cannot display a directory using FileLink. "
346 "Use FileLinks to display '%s'." % path)
363 "Use FileLinks to display '%s'." % path)
347 self.path = fsdecode(path)
364 self.path = fsdecode(path)
348 self.url_prefix = url_prefix
365 self.url_prefix = url_prefix
349 self.result_html_prefix = result_html_prefix
366 self.result_html_prefix = result_html_prefix
350 self.result_html_suffix = result_html_suffix
367 self.result_html_suffix = result_html_suffix
351
368
352 def _format_path(self):
369 def _format_path(self):
353 fp = ''.join([self.url_prefix, html_escape(self.path)])
370 fp = ''.join([self.url_prefix, html_escape(self.path)])
354 return ''.join([self.result_html_prefix,
371 return ''.join([self.result_html_prefix,
355 self.html_link_str % \
372 self.html_link_str % \
356 (fp, html_escape(self.path, quote=False)),
373 (fp, html_escape(self.path, quote=False)),
357 self.result_html_suffix])
374 self.result_html_suffix])
358
375
359 def _repr_html_(self):
376 def _repr_html_(self):
360 """return html link to file
377 """return html link to file
361 """
378 """
362 if not exists(self.path):
379 if not exists(self.path):
363 return ("Path (<tt>%s</tt>) doesn't exist. "
380 return ("Path (<tt>%s</tt>) doesn't exist. "
364 "It may still be in the process of "
381 "It may still be in the process of "
365 "being generated, or you may have the "
382 "being generated, or you may have the "
366 "incorrect path." % self.path)
383 "incorrect path." % self.path)
367
384
368 return self._format_path()
385 return self._format_path()
369
386
370 def __repr__(self):
387 def __repr__(self):
371 """return absolute path to file
388 """return absolute path to file
372 """
389 """
373 return abspath(self.path)
390 return abspath(self.path)
374
391
375 class FileLinks(FileLink):
392 class FileLinks(FileLink):
376 """Class for embedding local file links in an IPython session, based on path
393 """Class for embedding local file links in an IPython session, based on path
377
394
378 e.g. to embed links to files that were generated in the IPython notebook
395 e.g. to embed links to files that were generated in the IPython notebook
379 under ``my/data``, you would do::
396 under ``my/data``, you would do::
380
397
381 local_files = FileLinks("my/data")
398 local_files = FileLinks("my/data")
382 display(local_files)
399 display(local_files)
383
400
384 or in the HTML notebook, just::
401 or in the HTML notebook, just::
385
402
386 FileLinks("my/data")
403 FileLinks("my/data")
387 """
404 """
388 def __init__(self,
405 def __init__(self,
389 path,
406 path,
390 url_prefix='',
407 url_prefix='',
391 included_suffixes=None,
408 included_suffixes=None,
392 result_html_prefix='',
409 result_html_prefix='',
393 result_html_suffix='<br>',
410 result_html_suffix='<br>',
394 notebook_display_formatter=None,
411 notebook_display_formatter=None,
395 terminal_display_formatter=None,
412 terminal_display_formatter=None,
396 recursive=True):
413 recursive=True):
397 """
414 """
398 See :class:`FileLink` for the ``path``, ``url_prefix``,
415 See :class:`FileLink` for the ``path``, ``url_prefix``,
399 ``result_html_prefix`` and ``result_html_suffix`` parameters.
416 ``result_html_prefix`` and ``result_html_suffix`` parameters.
400
417
401 included_suffixes : list
418 included_suffixes : list
402 Filename suffixes to include when formatting output [default: include
419 Filename suffixes to include when formatting output [default: include
403 all files]
420 all files]
404
421
405 notebook_display_formatter : function
422 notebook_display_formatter : function
406 Used to format links for display in the notebook. See discussion of
423 Used to format links for display in the notebook. See discussion of
407 formatter functions below.
424 formatter functions below.
408
425
409 terminal_display_formatter : function
426 terminal_display_formatter : function
410 Used to format links for display in the terminal. See discussion of
427 Used to format links for display in the terminal. See discussion of
411 formatter functions below.
428 formatter functions below.
412
429
413 Formatter functions must be of the form::
430 Formatter functions must be of the form::
414
431
415 f(dirname, fnames, included_suffixes)
432 f(dirname, fnames, included_suffixes)
416
433
417 dirname : str
434 dirname : str
418 The name of a directory
435 The name of a directory
419 fnames : list
436 fnames : list
420 The files in that directory
437 The files in that directory
421 included_suffixes : list
438 included_suffixes : list
422 The file suffixes that should be included in the output (passing None
439 The file suffixes that should be included in the output (passing None
423 meansto include all suffixes in the output in the built-in formatters)
440 meansto include all suffixes in the output in the built-in formatters)
424 recursive : boolean
441 recursive : boolean
425 Whether to recurse into subdirectories. Default is True.
442 Whether to recurse into subdirectories. Default is True.
426
443
427 The function should return a list of lines that will be printed in the
444 The function should return a list of lines that will be printed in the
428 notebook (if passing notebook_display_formatter) or the terminal (if
445 notebook (if passing notebook_display_formatter) or the terminal (if
429 passing terminal_display_formatter). This function is iterated over for
446 passing terminal_display_formatter). This function is iterated over for
430 each directory in self.path. Default formatters are in place, can be
447 each directory in self.path. Default formatters are in place, can be
431 passed here to support alternative formatting.
448 passed here to support alternative formatting.
432
449
433 """
450 """
434 if isfile(path):
451 if isfile(path):
435 raise ValueError("Cannot display a file using FileLinks. "
452 raise ValueError("Cannot display a file using FileLinks. "
436 "Use FileLink to display '%s'." % path)
453 "Use FileLink to display '%s'." % path)
437 self.included_suffixes = included_suffixes
454 self.included_suffixes = included_suffixes
438 # remove trailing slashes for more consistent output formatting
455 # remove trailing slashes for more consistent output formatting
439 path = path.rstrip('/')
456 path = path.rstrip('/')
440
457
441 self.path = path
458 self.path = path
442 self.url_prefix = url_prefix
459 self.url_prefix = url_prefix
443 self.result_html_prefix = result_html_prefix
460 self.result_html_prefix = result_html_prefix
444 self.result_html_suffix = result_html_suffix
461 self.result_html_suffix = result_html_suffix
445
462
446 self.notebook_display_formatter = \
463 self.notebook_display_formatter = \
447 notebook_display_formatter or self._get_notebook_display_formatter()
464 notebook_display_formatter or self._get_notebook_display_formatter()
448 self.terminal_display_formatter = \
465 self.terminal_display_formatter = \
449 terminal_display_formatter or self._get_terminal_display_formatter()
466 terminal_display_formatter or self._get_terminal_display_formatter()
450
467
451 self.recursive = recursive
468 self.recursive = recursive
452
469
453 def _get_display_formatter(self,
470 def _get_display_formatter(self,
454 dirname_output_format,
471 dirname_output_format,
455 fname_output_format,
472 fname_output_format,
456 fp_format,
473 fp_format,
457 fp_cleaner=None):
474 fp_cleaner=None):
458 """ generate built-in formatter function
475 """ generate built-in formatter function
459
476
460 this is used to define both the notebook and terminal built-in
477 this is used to define both the notebook and terminal built-in
461 formatters as they only differ by some wrapper text for each entry
478 formatters as they only differ by some wrapper text for each entry
462
479
463 dirname_output_format: string to use for formatting directory
480 dirname_output_format: string to use for formatting directory
464 names, dirname will be substituted for a single "%s" which
481 names, dirname will be substituted for a single "%s" which
465 must appear in this string
482 must appear in this string
466 fname_output_format: string to use for formatting file names,
483 fname_output_format: string to use for formatting file names,
467 if a single "%s" appears in the string, fname will be substituted
484 if a single "%s" appears in the string, fname will be substituted
468 if two "%s" appear in the string, the path to fname will be
485 if two "%s" appear in the string, the path to fname will be
469 substituted for the first and fname will be substituted for the
486 substituted for the first and fname will be substituted for the
470 second
487 second
471 fp_format: string to use for formatting filepaths, must contain
488 fp_format: string to use for formatting filepaths, must contain
472 exactly two "%s" and the dirname will be subsituted for the first
489 exactly two "%s" and the dirname will be subsituted for the first
473 and fname will be substituted for the second
490 and fname will be substituted for the second
474 """
491 """
475 def f(dirname, fnames, included_suffixes=None):
492 def f(dirname, fnames, included_suffixes=None):
476 result = []
493 result = []
477 # begin by figuring out which filenames, if any,
494 # begin by figuring out which filenames, if any,
478 # are going to be displayed
495 # are going to be displayed
479 display_fnames = []
496 display_fnames = []
480 for fname in fnames:
497 for fname in fnames:
481 if (isfile(join(dirname,fname)) and
498 if (isfile(join(dirname,fname)) and
482 (included_suffixes is None or
499 (included_suffixes is None or
483 splitext(fname)[1] in included_suffixes)):
500 splitext(fname)[1] in included_suffixes)):
484 display_fnames.append(fname)
501 display_fnames.append(fname)
485
502
486 if len(display_fnames) == 0:
503 if len(display_fnames) == 0:
487 # if there are no filenames to display, don't print anything
504 # if there are no filenames to display, don't print anything
488 # (not even the directory name)
505 # (not even the directory name)
489 pass
506 pass
490 else:
507 else:
491 # otherwise print the formatted directory name followed by
508 # otherwise print the formatted directory name followed by
492 # the formatted filenames
509 # the formatted filenames
493 dirname_output_line = dirname_output_format % dirname
510 dirname_output_line = dirname_output_format % dirname
494 result.append(dirname_output_line)
511 result.append(dirname_output_line)
495 for fname in display_fnames:
512 for fname in display_fnames:
496 fp = fp_format % (dirname,fname)
513 fp = fp_format % (dirname,fname)
497 if fp_cleaner is not None:
514 if fp_cleaner is not None:
498 fp = fp_cleaner(fp)
515 fp = fp_cleaner(fp)
499 try:
516 try:
500 # output can include both a filepath and a filename...
517 # output can include both a filepath and a filename...
501 fname_output_line = fname_output_format % (fp, fname)
518 fname_output_line = fname_output_format % (fp, fname)
502 except TypeError:
519 except TypeError:
503 # ... or just a single filepath
520 # ... or just a single filepath
504 fname_output_line = fname_output_format % fname
521 fname_output_line = fname_output_format % fname
505 result.append(fname_output_line)
522 result.append(fname_output_line)
506 return result
523 return result
507 return f
524 return f
508
525
509 def _get_notebook_display_formatter(self,
526 def _get_notebook_display_formatter(self,
510 spacer="&nbsp;&nbsp;"):
527 spacer="&nbsp;&nbsp;"):
511 """ generate function to use for notebook formatting
528 """ generate function to use for notebook formatting
512 """
529 """
513 dirname_output_format = \
530 dirname_output_format = \
514 self.result_html_prefix + "%s/" + self.result_html_suffix
531 self.result_html_prefix + "%s/" + self.result_html_suffix
515 fname_output_format = \
532 fname_output_format = \
516 self.result_html_prefix + spacer + self.html_link_str + self.result_html_suffix
533 self.result_html_prefix + spacer + self.html_link_str + self.result_html_suffix
517 fp_format = self.url_prefix + '%s/%s'
534 fp_format = self.url_prefix + '%s/%s'
518 if sep == "\\":
535 if sep == "\\":
519 # Working on a platform where the path separator is "\", so
536 # Working on a platform where the path separator is "\", so
520 # must convert these to "/" for generating a URI
537 # must convert these to "/" for generating a URI
521 def fp_cleaner(fp):
538 def fp_cleaner(fp):
522 # Replace all occurrences of backslash ("\") with a forward
539 # Replace all occurrences of backslash ("\") with a forward
523 # slash ("/") - this is necessary on windows when a path is
540 # slash ("/") - this is necessary on windows when a path is
524 # provided as input, but we must link to a URI
541 # provided as input, but we must link to a URI
525 return fp.replace('\\','/')
542 return fp.replace('\\','/')
526 else:
543 else:
527 fp_cleaner = None
544 fp_cleaner = None
528
545
529 return self._get_display_formatter(dirname_output_format,
546 return self._get_display_formatter(dirname_output_format,
530 fname_output_format,
547 fname_output_format,
531 fp_format,
548 fp_format,
532 fp_cleaner)
549 fp_cleaner)
533
550
534 def _get_terminal_display_formatter(self,
551 def _get_terminal_display_formatter(self,
535 spacer=" "):
552 spacer=" "):
536 """ generate function to use for terminal formatting
553 """ generate function to use for terminal formatting
537 """
554 """
538 dirname_output_format = "%s/"
555 dirname_output_format = "%s/"
539 fname_output_format = spacer + "%s"
556 fname_output_format = spacer + "%s"
540 fp_format = '%s/%s'
557 fp_format = '%s/%s'
541
558
542 return self._get_display_formatter(dirname_output_format,
559 return self._get_display_formatter(dirname_output_format,
543 fname_output_format,
560 fname_output_format,
544 fp_format)
561 fp_format)
545
562
546 def _format_path(self):
563 def _format_path(self):
547 result_lines = []
564 result_lines = []
548 if self.recursive:
565 if self.recursive:
549 walked_dir = list(walk(self.path))
566 walked_dir = list(walk(self.path))
550 else:
567 else:
551 walked_dir = [next(walk(self.path))]
568 walked_dir = [next(walk(self.path))]
552 walked_dir.sort()
569 walked_dir.sort()
553 for dirname, subdirs, fnames in walked_dir:
570 for dirname, subdirs, fnames in walked_dir:
554 result_lines += self.notebook_display_formatter(dirname, fnames, self.included_suffixes)
571 result_lines += self.notebook_display_formatter(dirname, fnames, self.included_suffixes)
555 return '\n'.join(result_lines)
572 return '\n'.join(result_lines)
556
573
557 def __repr__(self):
574 def __repr__(self):
558 """return newline-separated absolute paths
575 """return newline-separated absolute paths
559 """
576 """
560 result_lines = []
577 result_lines = []
561 if self.recursive:
578 if self.recursive:
562 walked_dir = list(walk(self.path))
579 walked_dir = list(walk(self.path))
563 else:
580 else:
564 walked_dir = [next(walk(self.path))]
581 walked_dir = [next(walk(self.path))]
565 walked_dir.sort()
582 walked_dir.sort()
566 for dirname, subdirs, fnames in walked_dir:
583 for dirname, subdirs, fnames in walked_dir:
567 result_lines += self.terminal_display_formatter(dirname, fnames, self.included_suffixes)
584 result_lines += self.terminal_display_formatter(dirname, fnames, self.included_suffixes)
568 return '\n'.join(result_lines)
585 return '\n'.join(result_lines)
569
586
570
587
571 class Code(TextDisplayObject):
588 class Code(TextDisplayObject):
572 """Display syntax-highlighted source code.
589 """Display syntax-highlighted source code.
573
590
574 This uses Pygments to highlight the code for HTML and Latex output.
591 This uses Pygments to highlight the code for HTML and Latex output.
575
592
576 Parameters
593 Parameters
577 ----------
594 ----------
578 data : str
595 data : str
579 The code as a string
596 The code as a string
580 url : str
597 url : str
581 A URL to fetch the code from
598 A URL to fetch the code from
582 filename : str
599 filename : str
583 A local filename to load the code from
600 A local filename to load the code from
584 language : str
601 language : str
585 The short name of a Pygments lexer to use for highlighting.
602 The short name of a Pygments lexer to use for highlighting.
586 If not specified, it will guess the lexer based on the filename
603 If not specified, it will guess the lexer based on the filename
587 or the code. Available lexers: http://pygments.org/docs/lexers/
604 or the code. Available lexers: http://pygments.org/docs/lexers/
588 """
605 """
589 def __init__(self, data=None, url=None, filename=None, language=None):
606 def __init__(self, data=None, url=None, filename=None, language=None):
590 self.language = language
607 self.language = language
591 super().__init__(data=data, url=url, filename=filename)
608 super().__init__(data=data, url=url, filename=filename)
592
609
593 def _get_lexer(self):
610 def _get_lexer(self):
594 if self.language:
611 if self.language:
595 from pygments.lexers import get_lexer_by_name
612 from pygments.lexers import get_lexer_by_name
596 return get_lexer_by_name(self.language)
613 return get_lexer_by_name(self.language)
597 elif self.filename:
614 elif self.filename:
598 from pygments.lexers import get_lexer_for_filename
615 from pygments.lexers import get_lexer_for_filename
599 return get_lexer_for_filename(self.filename)
616 return get_lexer_for_filename(self.filename)
600 else:
617 else:
601 from pygments.lexers import guess_lexer
618 from pygments.lexers import guess_lexer
602 return guess_lexer(self.data)
619 return guess_lexer(self.data)
603
620
604 def __repr__(self):
621 def __repr__(self):
605 return self.data
622 return self.data
606
623
607 def _repr_html_(self):
624 def _repr_html_(self):
608 from pygments import highlight
625 from pygments import highlight
609 from pygments.formatters import HtmlFormatter
626 from pygments.formatters import HtmlFormatter
610 fmt = HtmlFormatter()
627 fmt = HtmlFormatter()
611 style = '<style>{}</style>'.format(fmt.get_style_defs('.output_html'))
628 style = '<style>{}</style>'.format(fmt.get_style_defs('.output_html'))
612 return style + highlight(self.data, self._get_lexer(), fmt)
629 return style + highlight(self.data, self._get_lexer(), fmt)
613
630
614 def _repr_latex_(self):
631 def _repr_latex_(self):
615 from pygments import highlight
632 from pygments import highlight
616 from pygments.formatters import LatexFormatter
633 from pygments.formatters import LatexFormatter
617 return highlight(self.data, self._get_lexer(), LatexFormatter())
634 return highlight(self.data, self._get_lexer(), LatexFormatter())
@@ -1,209 +1,253 b''
1 """Tests for IPython.lib.display.
1 """Tests for IPython.lib.display.
2
2
3 """
3 """
4 #-----------------------------------------------------------------------------
4 #-----------------------------------------------------------------------------
5 # Copyright (c) 2012, the IPython Development Team.
5 # Copyright (c) 2012, the IPython Development Team.
6 #
6 #
7 # Distributed under the terms of the Modified BSD License.
7 # Distributed under the terms of the Modified BSD License.
8 #
8 #
9 # The full license is in the file COPYING.txt, distributed with this software.
9 # The full license is in the file COPYING.txt, distributed with this software.
10 #-----------------------------------------------------------------------------
10 #-----------------------------------------------------------------------------
11
11
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13 # Imports
13 # Imports
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15 from tempfile import NamedTemporaryFile, mkdtemp
15 from tempfile import NamedTemporaryFile, mkdtemp
16 from os.path import split, join as pjoin, dirname
16 from os.path import split, join as pjoin, dirname
17 import sys
17 import sys
18 try:
18 try:
19 import pathlib
19 import pathlib
20 except ImportError:
20 except ImportError:
21 pass
21 pass
22 from unittest import mock
22 from unittest import TestCase, mock
23 import struct
24 import wave
25 from io import BytesIO
23
26
24 # Third-party imports
27 # Third-party imports
25 import nose.tools as nt
28 import nose.tools as nt
26 import numpy
29 import numpy
27
30
28 # Our own imports
31 # Our own imports
29 from IPython.lib import display
32 from IPython.lib import display
30
33
31 #-----------------------------------------------------------------------------
34 #-----------------------------------------------------------------------------
32 # Classes and functions
35 # Classes and functions
33 #-----------------------------------------------------------------------------
36 #-----------------------------------------------------------------------------
34
37
35 #--------------------------
38 #--------------------------
36 # FileLink tests
39 # FileLink tests
37 #--------------------------
40 #--------------------------
38
41
39 def test_instantiation_FileLink():
42 def test_instantiation_FileLink():
40 """FileLink: Test class can be instantiated"""
43 """FileLink: Test class can be instantiated"""
41 fl = display.FileLink('example.txt')
44 fl = display.FileLink('example.txt')
42 # TODO: remove if when only Python >= 3.6 is supported
45 # TODO: remove if when only Python >= 3.6 is supported
43 if sys.version_info >= (3, 6):
46 if sys.version_info >= (3, 6):
44 fl = display.FileLink(pathlib.PurePath('example.txt'))
47 fl = display.FileLink(pathlib.PurePath('example.txt'))
45
48
46 def test_warning_on_non_existant_path_FileLink():
49 def test_warning_on_non_existant_path_FileLink():
47 """FileLink: Calling _repr_html_ on non-existant files returns a warning
50 """FileLink: Calling _repr_html_ on non-existant files returns a warning
48 """
51 """
49 fl = display.FileLink('example.txt')
52 fl = display.FileLink('example.txt')
50 nt.assert_true(fl._repr_html_().startswith('Path (<tt>example.txt</tt>)'))
53 nt.assert_true(fl._repr_html_().startswith('Path (<tt>example.txt</tt>)'))
51
54
52 def test_existing_path_FileLink():
55 def test_existing_path_FileLink():
53 """FileLink: Calling _repr_html_ functions as expected on existing filepath
56 """FileLink: Calling _repr_html_ functions as expected on existing filepath
54 """
57 """
55 tf = NamedTemporaryFile()
58 tf = NamedTemporaryFile()
56 fl = display.FileLink(tf.name)
59 fl = display.FileLink(tf.name)
57 actual = fl._repr_html_()
60 actual = fl._repr_html_()
58 expected = "<a href='%s' target='_blank'>%s</a><br>" % (tf.name,tf.name)
61 expected = "<a href='%s' target='_blank'>%s</a><br>" % (tf.name,tf.name)
59 nt.assert_equal(actual,expected)
62 nt.assert_equal(actual,expected)
60
63
61 def test_existing_path_FileLink_repr():
64 def test_existing_path_FileLink_repr():
62 """FileLink: Calling repr() functions as expected on existing filepath
65 """FileLink: Calling repr() functions as expected on existing filepath
63 """
66 """
64 tf = NamedTemporaryFile()
67 tf = NamedTemporaryFile()
65 fl = display.FileLink(tf.name)
68 fl = display.FileLink(tf.name)
66 actual = repr(fl)
69 actual = repr(fl)
67 expected = tf.name
70 expected = tf.name
68 nt.assert_equal(actual,expected)
71 nt.assert_equal(actual,expected)
69
72
70 def test_error_on_directory_to_FileLink():
73 def test_error_on_directory_to_FileLink():
71 """FileLink: Raises error when passed directory
74 """FileLink: Raises error when passed directory
72 """
75 """
73 td = mkdtemp()
76 td = mkdtemp()
74 nt.assert_raises(ValueError,display.FileLink,td)
77 nt.assert_raises(ValueError,display.FileLink,td)
75
78
76 #--------------------------
79 #--------------------------
77 # FileLinks tests
80 # FileLinks tests
78 #--------------------------
81 #--------------------------
79
82
80 def test_instantiation_FileLinks():
83 def test_instantiation_FileLinks():
81 """FileLinks: Test class can be instantiated
84 """FileLinks: Test class can be instantiated
82 """
85 """
83 fls = display.FileLinks('example')
86 fls = display.FileLinks('example')
84
87
85 def test_warning_on_non_existant_path_FileLinks():
88 def test_warning_on_non_existant_path_FileLinks():
86 """FileLinks: Calling _repr_html_ on non-existant files returns a warning
89 """FileLinks: Calling _repr_html_ on non-existant files returns a warning
87 """
90 """
88 fls = display.FileLinks('example')
91 fls = display.FileLinks('example')
89 nt.assert_true(fls._repr_html_().startswith('Path (<tt>example</tt>)'))
92 nt.assert_true(fls._repr_html_().startswith('Path (<tt>example</tt>)'))
90
93
91 def test_existing_path_FileLinks():
94 def test_existing_path_FileLinks():
92 """FileLinks: Calling _repr_html_ functions as expected on existing dir
95 """FileLinks: Calling _repr_html_ functions as expected on existing dir
93 """
96 """
94 td = mkdtemp()
97 td = mkdtemp()
95 tf1 = NamedTemporaryFile(dir=td)
98 tf1 = NamedTemporaryFile(dir=td)
96 tf2 = NamedTemporaryFile(dir=td)
99 tf2 = NamedTemporaryFile(dir=td)
97 fl = display.FileLinks(td)
100 fl = display.FileLinks(td)
98 actual = fl._repr_html_()
101 actual = fl._repr_html_()
99 actual = actual.split('\n')
102 actual = actual.split('\n')
100 actual.sort()
103 actual.sort()
101 # the links should always have forward slashes, even on windows, so replace
104 # the links should always have forward slashes, even on windows, so replace
102 # backslashes with forward slashes here
105 # backslashes with forward slashes here
103 expected = ["%s/<br>" % td,
106 expected = ["%s/<br>" % td,
104 "&nbsp;&nbsp;<a href='%s' target='_blank'>%s</a><br>" %\
107 "&nbsp;&nbsp;<a href='%s' target='_blank'>%s</a><br>" %\
105 (tf2.name.replace("\\","/"),split(tf2.name)[1]),
108 (tf2.name.replace("\\","/"),split(tf2.name)[1]),
106 "&nbsp;&nbsp;<a href='%s' target='_blank'>%s</a><br>" %\
109 "&nbsp;&nbsp;<a href='%s' target='_blank'>%s</a><br>" %\
107 (tf1.name.replace("\\","/"),split(tf1.name)[1])]
110 (tf1.name.replace("\\","/"),split(tf1.name)[1])]
108 expected.sort()
111 expected.sort()
109 # We compare the sorted list of links here as that's more reliable
112 # We compare the sorted list of links here as that's more reliable
110 nt.assert_equal(actual,expected)
113 nt.assert_equal(actual,expected)
111
114
112 def test_existing_path_FileLinks_alt_formatter():
115 def test_existing_path_FileLinks_alt_formatter():
113 """FileLinks: Calling _repr_html_ functions as expected w/ an alt formatter
116 """FileLinks: Calling _repr_html_ functions as expected w/ an alt formatter
114 """
117 """
115 td = mkdtemp()
118 td = mkdtemp()
116 tf1 = NamedTemporaryFile(dir=td)
119 tf1 = NamedTemporaryFile(dir=td)
117 tf2 = NamedTemporaryFile(dir=td)
120 tf2 = NamedTemporaryFile(dir=td)
118 def fake_formatter(dirname,fnames,included_suffixes):
121 def fake_formatter(dirname,fnames,included_suffixes):
119 return ["hello","world"]
122 return ["hello","world"]
120 fl = display.FileLinks(td,notebook_display_formatter=fake_formatter)
123 fl = display.FileLinks(td,notebook_display_formatter=fake_formatter)
121 actual = fl._repr_html_()
124 actual = fl._repr_html_()
122 actual = actual.split('\n')
125 actual = actual.split('\n')
123 actual.sort()
126 actual.sort()
124 expected = ["hello","world"]
127 expected = ["hello","world"]
125 expected.sort()
128 expected.sort()
126 # We compare the sorted list of links here as that's more reliable
129 # We compare the sorted list of links here as that's more reliable
127 nt.assert_equal(actual,expected)
130 nt.assert_equal(actual,expected)
128
131
129 def test_existing_path_FileLinks_repr():
132 def test_existing_path_FileLinks_repr():
130 """FileLinks: Calling repr() functions as expected on existing directory """
133 """FileLinks: Calling repr() functions as expected on existing directory """
131 td = mkdtemp()
134 td = mkdtemp()
132 tf1 = NamedTemporaryFile(dir=td)
135 tf1 = NamedTemporaryFile(dir=td)
133 tf2 = NamedTemporaryFile(dir=td)
136 tf2 = NamedTemporaryFile(dir=td)
134 fl = display.FileLinks(td)
137 fl = display.FileLinks(td)
135 actual = repr(fl)
138 actual = repr(fl)
136 actual = actual.split('\n')
139 actual = actual.split('\n')
137 actual.sort()
140 actual.sort()
138 expected = ['%s/' % td, ' %s' % split(tf1.name)[1],' %s' % split(tf2.name)[1]]
141 expected = ['%s/' % td, ' %s' % split(tf1.name)[1],' %s' % split(tf2.name)[1]]
139 expected.sort()
142 expected.sort()
140 # We compare the sorted list of links here as that's more reliable
143 # We compare the sorted list of links here as that's more reliable
141 nt.assert_equal(actual,expected)
144 nt.assert_equal(actual,expected)
142
145
143 def test_existing_path_FileLinks_repr_alt_formatter():
146 def test_existing_path_FileLinks_repr_alt_formatter():
144 """FileLinks: Calling repr() functions as expected w/ alt formatter
147 """FileLinks: Calling repr() functions as expected w/ alt formatter
145 """
148 """
146 td = mkdtemp()
149 td = mkdtemp()
147 tf1 = NamedTemporaryFile(dir=td)
150 tf1 = NamedTemporaryFile(dir=td)
148 tf2 = NamedTemporaryFile(dir=td)
151 tf2 = NamedTemporaryFile(dir=td)
149 def fake_formatter(dirname,fnames,included_suffixes):
152 def fake_formatter(dirname,fnames,included_suffixes):
150 return ["hello","world"]
153 return ["hello","world"]
151 fl = display.FileLinks(td,terminal_display_formatter=fake_formatter)
154 fl = display.FileLinks(td,terminal_display_formatter=fake_formatter)
152 actual = repr(fl)
155 actual = repr(fl)
153 actual = actual.split('\n')
156 actual = actual.split('\n')
154 actual.sort()
157 actual.sort()
155 expected = ["hello","world"]
158 expected = ["hello","world"]
156 expected.sort()
159 expected.sort()
157 # We compare the sorted list of links here as that's more reliable
160 # We compare the sorted list of links here as that's more reliable
158 nt.assert_equal(actual,expected)
161 nt.assert_equal(actual,expected)
159
162
160 def test_error_on_file_to_FileLinks():
163 def test_error_on_file_to_FileLinks():
161 """FileLinks: Raises error when passed file
164 """FileLinks: Raises error when passed file
162 """
165 """
163 td = mkdtemp()
166 td = mkdtemp()
164 tf1 = NamedTemporaryFile(dir=td)
167 tf1 = NamedTemporaryFile(dir=td)
165 nt.assert_raises(ValueError,display.FileLinks,tf1.name)
168 nt.assert_raises(ValueError,display.FileLinks,tf1.name)
166
169
167 def test_recursive_FileLinks():
170 def test_recursive_FileLinks():
168 """FileLinks: Does not recurse when recursive=False
171 """FileLinks: Does not recurse when recursive=False
169 """
172 """
170 td = mkdtemp()
173 td = mkdtemp()
171 tf = NamedTemporaryFile(dir=td)
174 tf = NamedTemporaryFile(dir=td)
172 subtd = mkdtemp(dir=td)
175 subtd = mkdtemp(dir=td)
173 subtf = NamedTemporaryFile(dir=subtd)
176 subtf = NamedTemporaryFile(dir=subtd)
174 fl = display.FileLinks(td)
177 fl = display.FileLinks(td)
175 actual = str(fl)
178 actual = str(fl)
176 actual = actual.split('\n')
179 actual = actual.split('\n')
177 nt.assert_equal(len(actual), 4, actual)
180 nt.assert_equal(len(actual), 4, actual)
178 fl = display.FileLinks(td, recursive=False)
181 fl = display.FileLinks(td, recursive=False)
179 actual = str(fl)
182 actual = str(fl)
180 actual = actual.split('\n')
183 actual = actual.split('\n')
181 nt.assert_equal(len(actual), 2, actual)
184 nt.assert_equal(len(actual), 2, actual)
182
185
183 def test_audio_from_file():
186 def test_audio_from_file():
184 path = pjoin(dirname(__file__), 'test.wav')
187 path = pjoin(dirname(__file__), 'test.wav')
185 display.Audio(filename=path)
188 display.Audio(filename=path)
186
189
187 def test_audio_from_numpy_array():
190 class TestAudioDataWithNumpy(TestCase):
188 display.Audio(get_test_tone(), rate=44100)
191 def test_audio_from_numpy_array(self):
189
192 test_tone = get_test_tone()
190 def test_audio_from_list_without_numpy():
193 audio = display.Audio(test_tone, rate=44100)
191 # Simulate numpy not installed.
194 nt.assert_equal(len(read_wav(audio.data)), len(test_tone))
192 with mock.patch('numpy.array', side_effect=ImportError):
195
193 display.Audio(list(get_test_tone()), rate=44100)
196 def test_audio_from_list(self):
194
197 test_tone = get_test_tone()
195 def test_audio_from_list_without_numpy_raises_for_nested_list():
198 audio = display.Audio(list(test_tone), rate=44100)
196 # Simulate numpy not installed.
199 nt.assert_equal(len(read_wav(audio.data)), len(test_tone))
197 with mock.patch('numpy.array', side_effect=ImportError):
200
201 def test_audio_from_numpy_array_without_rate_raises(self):
202 nt.assert_raises(ValueError, display.Audio, get_test_tone())
203
204 def test_audio_data_normalization(self):
205 expected_max_value = numpy.iinfo(numpy.int16).max
206 for scale in [1, 0.5, 2]:
207 audio = display.Audio(get_test_tone(scale), rate=44100)
208 actual_max_value = numpy.max(numpy.abs(read_wav(audio.data)))
209 nt.assert_equal(actual_max_value, expected_max_value)
210
211 def test_audio_data_without_normalization(self):
212 max_int16 = numpy.iinfo(numpy.int16).max
213 for scale in [1, 0.5, 0.2]:
214 test_tone = get_test_tone(scale)
215 test_tone_max_abs = numpy.max(numpy.abs(test_tone))
216 expected_max_value = int(max_int16 * test_tone_max_abs)
217 audio = display.Audio(test_tone, rate=44100, normalize=False)
218 actual_max_value = numpy.max(numpy.abs(read_wav(audio.data)))
219 nt.assert_equal(actual_max_value, expected_max_value)
220
221 def test_audio_data_without_normalization_raises_for_invalid_data(self):
222 nt.assert_raises(
223 ValueError,
224 lambda: display.Audio([1.001], rate=44100, normalize=False))
225 nt.assert_raises(
226 ValueError,
227 lambda: display.Audio([-1.001], rate=44100, normalize=False))
228
229 def simulate_numpy_not_installed():
230 return mock.patch('numpy.array', mock.MagicMock(side_effect=ImportError))
231
232 @simulate_numpy_not_installed()
233 class TestAudioDataWithoutNumpy(TestAudioDataWithNumpy):
234 # All tests from `TestAudioDataWithNumpy` are inherited.
235
236 def test_audio_raises_for_nested_list(self):
198 stereo_signal = [list(get_test_tone())] * 2
237 stereo_signal = [list(get_test_tone())] * 2
199 nt.assert_raises(TypeError, lambda: display.Audio(stereo_signal, rate=44100))
238 nt.assert_raises(
200
239 TypeError,
201 def test_audio_from_numpy_array_without_rate_raises():
240 lambda: display.Audio(stereo_signal, rate=44100))
202 nt.assert_raises(ValueError, display.Audio, get_test_tone())
241
203
242 def get_test_tone(scale=1):
204 def get_test_tone():
243 return numpy.sin(2 * numpy.pi * 440 * numpy.linspace(0, 1, 44100)) * scale
205 return numpy.sin(2 * numpy.pi * 440 * numpy.linspace(0, 1, 44100))
244
245 def read_wav(data):
246 with wave.open(BytesIO(data)) as wave_file:
247 wave_data = wave_file.readframes(wave_file.getnframes())
248 num_samples = wave_file.getnframes() * wave_file.getnchannels()
249 return struct.unpack('<%sh' % num_samples, wave_data)
206
250
207 def test_code_from_file():
251 def test_code_from_file():
208 c = display.Code(filename=__file__)
252 c = display.Code(filename=__file__)
209 assert c._repr_html_().startswith('<style>')
253 assert c._repr_html_().startswith('<style>')
General Comments 0
You need to be logged in to leave comments. Login now