diff --git a/doc/users/whats_new/abstract_movie_writer.rst b/doc/users/whats_new/abstract_movie_writer.rst new file mode 100644 index 000000000000..44dc7bd5f182 --- /dev/null +++ b/doc/users/whats_new/abstract_movie_writer.rst @@ -0,0 +1,8 @@ +Abstract base class for movie writers +------------------------------------- + +The new :class:`~matplotlib.animation.AbstractMovieWriter` class defines +the API required by a class that is to be used as the `writer` in the +`save` method of the :class:`~matplotlib.animation.Animation` class. +The existing :class:`~matplotlib.animation.MovieWriter` class now derives +from the new abstract base class. diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index 2e1f2ff7657d..57e054171196 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -33,6 +33,7 @@ except ImportError: # python2 from base64 import encodestring as encodebytes +import abc import contextlib import tempfile from matplotlib.cbook import iterable, is_string_like @@ -91,7 +92,66 @@ def __getitem__(self, name): writers = MovieWriterRegistry() -class MovieWriter(object): +class AbstractMovieWriter(six.with_metaclass(abc.ABCMeta)): + ''' + Abstract base class for writing movies. Fundamentally, what a MovieWriter + does is provide is a way to grab frames by calling grab_frame(). + + setup() is called to start the process and finish() is called afterwards. + + This class is set up to provide for writing movie frame data to a pipe. + saving() is provided as a context manager to facilitate this process as:: + + with moviewriter.saving(fig, outfile='myfile.mp4', dpi=100): + # Iterate over frames + moviewriter.grab_frame(**savefig_kwargs) + + The use of the context manager ensures that setup() and finish() are + performed as necessary. + + An instance of a concrete subclass of this class can be given as the + `writer` argument of `Animation.save()`. + ''' + + @abc.abstractmethod + def setup(self, fig, outfile, dpi, *args): + ''' + Perform setup for writing the movie file. + + fig: `matplotlib.Figure` instance + The figure object that contains the information for frames + outfile: string + The filename of the resulting movie file + dpi: int + The DPI (or resolution) for the file. This controls the size + in pixels of the resulting movie file. + ''' + + @abc.abstractmethod + def grab_frame(self, **savefig_kwargs): + ''' + Grab the image information from the figure and save as a movie frame. + All keyword arguments in savefig_kwargs are passed on to the 'savefig' + command that saves the figure. + ''' + + @abc.abstractmethod + def finish(self): + 'Finish any processing for writing the movie.' + + @contextlib.contextmanager + def saving(self, fig, outfile, dpi, *args): + ''' + Context manager to facilitate writing the movie file. + + All arguments are passed on to `setup`. + ''' + self.setup(fig, outfile, dpi, *args) + yield + self.finish() + + +class MovieWriter(AbstractMovieWriter): ''' Base class for writing movies. Fundamentally, what a MovieWriter does is provide is a way to grab frames by calling grab_frame(). setup() @@ -99,9 +159,9 @@ class MovieWriter(object): This class is set up to provide for writing movie frame data to a pipe. saving() is provided as a context manager to facilitate this process as:: - with moviewriter.saving('myfile.mp4'): + with moviewriter.saving(fig, outfile='myfile.mp4', dpi=100): # Iterate over frames - moviewriter.grab_frame() + moviewriter.grab_frame(**savefig_kwargs) The use of the context manager ensures that setup and cleanup are performed as necessary. @@ -183,18 +243,6 @@ def setup(self, fig, outfile, dpi, *args): # eliminates the need for temp files. self._run() - @contextlib.contextmanager - def saving(self, *args): - ''' - Context manager to facilitate writing the movie file. - - ``*args`` are any parameters that should be passed to `setup`. - ''' - # This particular sequence is what contextlib.contextmanager wants - self.setup(*args) - yield - self.finish() - def _run(self): # Uses subprocess to call the program for assembling frames into a # movie file. *args* returns the sequence of command line arguments @@ -669,10 +717,10 @@ def save(self, filename, writer=None, fps=None, dpi=None, codec=None, *filename* is the output filename, e.g., :file:`mymovie.mp4` - *writer* is either an instance of :class:`MovieWriter` or a string - key that identifies a class to use, such as 'ffmpeg' or 'mencoder'. - If nothing is passed, the value of the rcparam `animation.writer` is - used. + *writer* is either an instance of :class:`AbstractMovieWriter` or + a string key that identifies a class to use, such as 'ffmpeg' or + 'mencoder'. If nothing is passed, the value of the rcparam + `animation.writer` is used. *fps* is the frames per second in the movie. Defaults to None, which will use the animation's specified interval to set the frames diff --git a/lib/matplotlib/tests/test_animation.py b/lib/matplotlib/tests/test_animation.py index 4118ab7b797e..2a62cfb7a2ac 100644 --- a/lib/matplotlib/tests/test_animation.py +++ b/lib/matplotlib/tests/test_animation.py @@ -6,6 +6,7 @@ import os import tempfile import numpy as np +from numpy.testing import assert_equal from nose import with_setup from matplotlib import pyplot as plt from matplotlib import animation @@ -14,10 +15,87 @@ from matplotlib.testing.decorators import CleanupTest +class NullMovieWriter(animation.AbstractMovieWriter): + """ + A minimal MovieWriter. It doesn't actually write anything. + It just saves the arguments that were given to the setup() and + grab_frame() methods as attributes, and counts how many times + grab_frame() is called. + + This class doesn't have an __init__ method with the appropriate + signature, and it doesn't define an isAvailable() method, so + it cannot be added to the 'writers' registry. + """ + + def setup(self, fig, outfile, dpi, *args): + self.fig = fig + self.outfile = outfile + self.dpi = dpi + self.args = args + self._count = 0 + + def grab_frame(self, **savefig_kwargs): + self.savefig_kwargs = savefig_kwargs + self._count += 1 + + def finish(self): + pass + + +def test_null_movie_writer(): + # Test running an animation with NullMovieWriter. + + fig = plt.figure() + + def init(): + pass + + def animate(i): + pass + + num_frames = 5 + filename = "unused.null" + fps = 30 + dpi = 50 + savefig_kwargs = dict(foo=0) + + anim = animation.FuncAnimation(fig, animate, init_func=init, + frames=num_frames) + writer = NullMovieWriter() + anim.save(filename, fps=fps, dpi=dpi, writer=writer, + savefig_kwargs=savefig_kwargs) + + assert_equal(writer.fig, fig) + assert_equal(writer.outfile, filename) + assert_equal(writer.dpi, dpi) + assert_equal(writer.args, ()) + assert_equal(writer.savefig_kwargs, savefig_kwargs) + assert_equal(writer._count, num_frames) + + +@animation.writers.register('null') +class RegisteredNullMovieWriter(NullMovieWriter): + + # To be able to add NullMovieWriter to the 'writers' registry, + # we must define an __init__ method with a specific signature, + # and we must define the class method isAvailable(). + # (These methods are not actually required to use an instance + # of this class as the 'writer' argument of Animation.save().) + + def __init__(self, fps=None, codec=None, bitrate=None, + extra_args=None, metadata=None): + pass + + @classmethod + def isAvailable(self): + return True + + WRITER_OUTPUT = dict(ffmpeg='mp4', ffmpeg_file='mp4', mencoder='mp4', mencoder_file='mp4', avconv='mp4', avconv_file='mp4', - imagemagick='gif', imagemagick_file='gif') + imagemagick='gif', imagemagick_file='gif', + null='null') # Smoke test for saving animations. In the future, we should probably