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