ENH: Create an abstract base class for movie writers. #5454

Merged
merged 3 commits into from Dec 29, 2015
@@ -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.
@@ -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,17 +92,76 @@ 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()
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('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
@@ -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