8000 Merge pull request #5454 from WarrenWeckesser/abstract-movie-writer · matplotlib/matplotlib@a4aebe3 · GitHub
[go: up one dir, main page]

Skip to content

Commit a4aebe3

Browse files
committed
Merge pull request #5454 from WarrenWeckesser/abstract-movie-writer
ENH: Create an abstract base class for movie writers.
2 parents 723d015 + 6bae745 commit a4aebe3

File tree

3 files changed

+154
-20
lines changed

3 files changed

+154
-20
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Abstract base class for movie writers
2+
-------------------------------------
3+
4+
The new :class:`~matplotlib.animation.AbstractMovieWriter` class defines
5+
the API required by a class that is to be used as the `writer` in the
6+
`save` method of the :class:`~matplotlib.animation.Animation` class.
7+
The existing :class:`~matplotlib.animation.MovieWriter` class now derives
8+
from the new abstract base class.

lib/matplotlib/animation.py

Lines changed: 67 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
except ImportError:
3434
# python2
3535
from base64 import encodestring as encodebytes
36+
import abc
3637
import contextlib
3738
import tempfile
3839
from matplotlib.cbook import iterable, is_string_like
@@ -91,17 +92,76 @@ def __getitem__(self, name):
9192
writers = MovieWriterRegistry()
9293

9394

94-
class MovieWriter(object):
95+
class AbstractMovieWriter(six.with_metaclass(abc.ABCMeta)):
96+
'''
97+
Abstract base class for writing movies. Fundamentally, what a MovieWriter
98+
does is provide is a way to grab frames by calling grab_frame().
99+
100+
setup() is called to start the process and finish() is called afterwards.
101+
102+
This class is set up to provide for writing movie frame data to a pipe.
103+
saving() is provided as a context manager to facilitate this process as::
104+
105+
with moviewriter.saving(fig, outfile='myfile.mp4', dpi=100):
106+
# Iterate over frames
107+
moviewriter.grab_frame(**savefig_kwargs)
108+
109+
The use of the context manager ensures that setup() and finish() are
110+
performed as necessary.
111+
112+
An instance of a concrete subclass of this class can be given as the
113+
`writer` argument of `Animation.save()`.
114+
'''
115+
116+
@abc.abstractmethod
117+
def setup(self, fig, outfile, dpi, *args):
118+
'''
119+
Perform setup for writing the movie file.
120+
121+
fig: `matplotlib.Figure` instance
122+
The figure object that contains the information for frames
123+
outfile: string
124+
The filename of the resulting movie file
125+
dpi: int
126+
The DPI (or resolution) for the file. This controls the size
127+
in pixels of the resulting movie file.
128+
'''
129+
130+
@abc.abstractmethod
131+
def grab_frame(self, **savefig_kwargs):
132+
'''
133+
Grab the image information from the figure and save as a movie frame.
134+
All keyword arguments in savefig_kwargs are passed on to the 'savefig'
135+
command that saves the figure.
136+
'''
137+
138+
@abc.abstractmethod
139+
def finish(self):
140+
'Finish any processing for writing the movie.'
141+
142+
@contextlib.contextmanager
143+
def saving(self, fig, outfile, dpi, *args):
144+
'''
145+
Context manager to facilitate writing the movie file.
146+
147+
All arguments are passed on to `setup`.
148+
'''
149+
self.setup(fig, outfile, dpi, *args)
150+
yield
151+
self.finish()
152+
153+
154+
class MovieWriter(AbstractMovieWriter):
95155
'''
96156
Base class for writing movies. Fundamentally, what a MovieWriter does
97157
is provide is a way to grab frames by calling grab_frame(). setup()
98158
is called to start the process and finish() is called afterwards.
99159
This class is set up to provide for writing movie frame data to a pipe.
100160
saving() is provided as a context manager to facilitate this process as::
101161
102-
with moviewriter.saving('myfile.mp4'):
162+
with moviewriter.saving(fig, outfile='myfile.mp4', dpi=100):
103163
# Iterate over frames
104-
moviewriter.grab_frame()
164+
moviewriter.grab_frame(**savefig_kwargs)
105165
106166
The use of the context manager ensures that setup and cleanup are
107167
performed as necessary.
@@ -183,18 +243,6 @@ def setup(self, fig, outfile, dpi, *args):
183243
# eliminates the need for temp files.
184244
self._run()
185245

186-
@contextlib.contextmanager
187-
def saving(self, *args):
188-
'''
189-
Context manager to facilitate writing the movie file.
190-
191-
``*args`` are any parameters that should be passed to `setup`.
192-
'''
193-
# This particular sequence is what contextlib.contextmanager wants
194-
self.setup(*args)
195-
yield
196-
self.finish()
197-
198246
def _run(self):
199247
# Uses subprocess to call the program for assembling frames into a
200248
# 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,
669717
670718
*filename* is the output filename, e.g., :file:`mymovie.mp4`
671719
672-
*writer* is either an instance of :class:`MovieWriter` or a string
673-
key that identifies a class to use, such as 'ffmpeg' or 'mencoder'.
674-
If nothing is passed, the value of the rcparam `animation.writer` is
675-
used.
720+
*writer* is either an instance of :class:`AbstractMovieWriter` or
721+
a string key that identifies a class to use, such as 'ffmpeg' or
722+
'mencoder'. If nothing is passed, the value of the rcparam
723+
`animation.writer` is used.
676724
677725
*fps* is the frames per second in the movie. Defaults to None,
678726
which will use the animation's specified interval to set the frames

lib/matplotlib/tests/test_animation.py

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import os
77
import tempfile
88
import numpy as np
9+
from numpy.testing import assert_equal
910
from nose import with_setup
1011
from matplotlib import pyplot as plt
1112
from matplotlib import animation
@@ -14,10 +15,87 @@
1415
from matplotlib.testing.decorators import CleanupTest
1516

1617

18+
class NullMovieWriter(animation.AbstractMovieWriter):
19+
"""
20+
A minimal MovieWriter. It doesn't actually write anything.
21+
It just saves the arguments that were given to the setup() and
22+
grab_frame() methods as attributes, and counts how many times
23+
grab_frame() is called.
24+
25+
This class doesn't have an __init__ method with the appropriate
26+
signature, and it doesn't define an isAvailable() method, so
27+
it cannot be added to the 'writers' registry.
28+
"""
29+
30+
def setup(self, fig, outfile, dpi, *args):
31+
self.fig = fig
32+
self.outfile = outfile
33+
self.dpi = dpi
34+
self.args = args
35+
self._count = 0
36+
37+
def grab_frame(self, **savefig_kwargs):
38+
self.savefig_kwargs = savefig_kwargs
39+
self._count += 1
40+
41+
def finish(self):
42+
pass
43+
44+
45+
def test_null_movie_writer():
46+
# Test running an animation with NullMovieWriter.
47+
48+
fig = plt.figure()
49+
50+
def init():
51+
pass
52+
53+
def animate(i):
54+
pass
55+
56+
num_frames = 5
57+
filename = "unused.null"
58+
fps = 30
59+
dpi = 50
60+
savefig_kwargs = dict(foo=0)
61+
62+
anim = animation.FuncAnimation(fig, animate, init_func=init,
63+
frames=num_frames)
64+
writer = NullMovieWriter()
65+
anim.save(filename, fps=fps, dpi=dpi, writer=writer,
66+
savefig_kwargs=savefig_kwargs)
67+
68+
assert_equal(writer.fig, fig)
69+
assert_equal(writer.outfile, filename)
70+
assert_equal(writer.dpi, dpi)
71+
assert_equal(writer.args, ())
72+
assert_equal(writer.savefig_kwargs, savefig_kwargs)
73+
assert_equal(writer._count, num_frames)
74+
75+
76+
@animation.writers.register('null')
77+
class RegisteredNullMovieWriter(NullMovieWriter):
78+
79+
# To be able to add NullMovieWriter to the 'writers' registry,
80+
# we must define an __init__ method with a specific signature,
81+
# and we must define the class method isAvailable().
82+
# (These methods are not actually required to use an instance
83+
# of this class as the 'writer' argument of Animation.save().)
84+
85+
def __init__(self, fps=None, codec=None, bitrate=None,
86+
extra_args=None, metadata=None):
87+
pass
88+
89+
@classmethod
90+
def isAvailable(self):
91+
return True
92+
93+
1794
WRITER_OUTPUT = dict(ffmpeg='mp4', ffmpeg_file='mp4',
1895
mencoder='mp4', mencoder_file='mp4',
1996
avconv='mp4', avconv_file='mp4',
20-
imagemagick='gif', imagemagick_file='gif')
97+
imagemagick='gif', imagemagick_file='gif',
98+
null='null')
2199

22100

23101
# Smoke test for saving animations. In the future, we should probably

0 commit comments

Comments
 (0)
0