You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			572 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			572 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Python
		
	
import os
 | 
						|
from pathlib import Path
 | 
						|
import platform
 | 
						|
import re
 | 
						|
import shutil
 | 
						|
import subprocess
 | 
						|
import sys
 | 
						|
import weakref
 | 
						|
 | 
						|
import numpy as np
 | 
						|
import pytest
 | 
						|
 | 
						|
import matplotlib as mpl
 | 
						|
from matplotlib import pyplot as plt
 | 
						|
from matplotlib import animation
 | 
						|
from matplotlib.animation import PillowWriter
 | 
						|
from matplotlib.testing.decorators import check_figures_equal
 | 
						|
 | 
						|
 | 
						|
@pytest.fixture()
 | 
						|
def anim(request):
 | 
						|
    """Create a simple animation (with options)."""
 | 
						|
    fig, ax = plt.subplots()
 | 
						|
    line, = ax.plot([], [])
 | 
						|
 | 
						|
    ax.set_xlim(0, 10)
 | 
						|
    ax.set_ylim(-1, 1)
 | 
						|
 | 
						|
    def init():
 | 
						|
        line.set_data([], [])
 | 
						|
        return line,
 | 
						|
 | 
						|
    def animate(i):
 | 
						|
        x = np.linspace(0, 10, 100)
 | 
						|
        y = np.sin(x + i)
 | 
						|
        line.set_data(x, y)
 | 
						|
        return line,
 | 
						|
 | 
						|
    # "klass" can be passed to determine the class returned by the fixture
 | 
						|
    kwargs = dict(getattr(request, 'param', {}))  # make a copy
 | 
						|
    klass = kwargs.pop('klass', animation.FuncAnimation)
 | 
						|
    if 'frames' not in kwargs:
 | 
						|
        kwargs['frames'] = 5
 | 
						|
    return klass(fig=fig, func=animate, init_func=init, **kwargs)
 | 
						|
 | 
						|
 | 
						|
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):
 | 
						|
        from matplotlib.animation import _validate_grabframe_kwargs
 | 
						|
        _validate_grabframe_kwargs(savefig_kwargs)
 | 
						|
        self.savefig_kwargs = savefig_kwargs
 | 
						|
        self._count += 1
 | 
						|
 | 
						|
    def finish(self):
 | 
						|
        pass
 | 
						|
 | 
						|
 | 
						|
def test_null_movie_writer(anim):
 | 
						|
    # Test running an animation with NullMovieWriter.
 | 
						|
    plt.rcParams["savefig.facecolor"] = "auto"
 | 
						|
    filename = "unused.null"
 | 
						|
    dpi = 50
 | 
						|
    savefig_kwargs = dict(foo=0)
 | 
						|
    writer = NullMovieWriter()
 | 
						|
 | 
						|
    anim.save(filename, dpi=dpi, writer=writer,
 | 
						|
              savefig_kwargs=savefig_kwargs)
 | 
						|
 | 
						|
    assert writer.fig == plt.figure(1)  # The figure used by anim fixture
 | 
						|
    assert writer.outfile == filename
 | 
						|
    assert writer.dpi == dpi
 | 
						|
    assert writer.args == ()
 | 
						|
    # we enrich the savefig kwargs to ensure we composite transparent
 | 
						|
    # output to an opaque background
 | 
						|
    for k, v in savefig_kwargs.items():
 | 
						|
        assert writer.savefig_kwargs[k] == v
 | 
						|
    assert writer._count == anim._save_count
 | 
						|
 | 
						|
 | 
						|
@pytest.mark.parametrize('anim', [dict(klass=dict)], indirect=['anim'])
 | 
						|
def test_animation_delete(anim):
 | 
						|
    if platform.python_implementation() == 'PyPy':
 | 
						|
        # Something in the test setup fixture lingers around into the test and
 | 
						|
        # breaks pytest.warns on PyPy. This garbage collection fixes it.
 | 
						|
        # https://foss.heptapod.net/pypy/pypy/-/issues/3536
 | 
						|
        np.testing.break_cycles()
 | 
						|
    anim = animation.FuncAnimation(**anim)
 | 
						|
    with pytest.warns(Warning, match='Animation was deleted'):
 | 
						|
        del anim
 | 
						|
        np.testing.break_cycles()
 | 
						|
 | 
						|
 | 
						|
def test_movie_writer_dpi_default():
 | 
						|
    class DummyMovieWriter(animation.MovieWriter):
 | 
						|
        def _run(self):
 | 
						|
            pass
 | 
						|
 | 
						|
    # Test setting up movie writer with figure.dpi default.
 | 
						|
    fig = plt.figure()
 | 
						|
 | 
						|
    filename = "unused.null"
 | 
						|
    fps = 5
 | 
						|
    codec = "unused"
 | 
						|
    bitrate = 1
 | 
						|
    extra_args = ["unused"]
 | 
						|
 | 
						|
    writer = DummyMovieWriter(fps, codec, bitrate, extra_args)
 | 
						|
    writer.setup(fig, filename)
 | 
						|
    assert writer.dpi == fig.dpi
 | 
						|
 | 
						|
 | 
						|
@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(cls):
 | 
						|
        return True
 | 
						|
 | 
						|
 | 
						|
WRITER_OUTPUT = [
 | 
						|
    ('ffmpeg', 'movie.mp4'),
 | 
						|
    ('ffmpeg_file', 'movie.mp4'),
 | 
						|
    ('imagemagick', 'movie.gif'),
 | 
						|
    ('imagemagick_file', 'movie.gif'),
 | 
						|
    ('pillow', 'movie.gif'),
 | 
						|
    ('html', 'movie.html'),
 | 
						|
    ('null', 'movie.null')
 | 
						|
]
 | 
						|
 | 
						|
 | 
						|
def gen_writers():
 | 
						|
    for writer, output in WRITER_OUTPUT:
 | 
						|
        if not animation.writers.is_available(writer):
 | 
						|
            mark = pytest.mark.skip(
 | 
						|
                f"writer '{writer}' not available on this system")
 | 
						|
            yield pytest.param(writer, None, output, marks=[mark])
 | 
						|
            yield pytest.param(writer, None, Path(output), marks=[mark])
 | 
						|
            continue
 | 
						|
 | 
						|
        writer_class = animation.writers[writer]
 | 
						|
        for frame_format in getattr(writer_class, 'supported_formats', [None]):
 | 
						|
            yield writer, frame_format, output
 | 
						|
            yield writer, frame_format, Path(output)
 | 
						|
 | 
						|
 | 
						|
# Smoke test for saving animations.  In the future, we should probably
 | 
						|
# design more sophisticated tests which compare resulting frames a-la
 | 
						|
# matplotlib.testing.image_comparison
 | 
						|
@pytest.mark.parametrize('writer, frame_format, output', gen_writers())
 | 
						|
@pytest.mark.parametrize('anim', [dict(klass=dict)], indirect=['anim'])
 | 
						|
def test_save_animation_smoketest(tmpdir, writer, frame_format, output, anim):
 | 
						|
    if frame_format is not None:
 | 
						|
        plt.rcParams["animation.frame_format"] = frame_format
 | 
						|
    anim = animation.FuncAnimation(**anim)
 | 
						|
    dpi = None
 | 
						|
    codec = None
 | 
						|
    if writer == 'ffmpeg':
 | 
						|
        # Issue #8253
 | 
						|
        anim._fig.set_size_inches((10.85, 9.21))
 | 
						|
        dpi = 100.
 | 
						|
        codec = 'h264'
 | 
						|
 | 
						|
    # Use temporary directory for the file-based writers, which produce a file
 | 
						|
    # per frame with known names.
 | 
						|
    with tmpdir.as_cwd():
 | 
						|
        anim.save(output, fps=30, writer=writer, bitrate=500, dpi=dpi,
 | 
						|
                  codec=codec)
 | 
						|
 | 
						|
    del anim
 | 
						|
 | 
						|
 | 
						|
@pytest.mark.parametrize('writer, frame_format, output', gen_writers())
 | 
						|
def test_grabframe(tmpdir, writer, frame_format, output):
 | 
						|
    WriterClass = animation.writers[writer]
 | 
						|
 | 
						|
    if frame_format is not None:
 | 
						|
        plt.rcParams["animation.frame_format"] = frame_format
 | 
						|
 | 
						|
    fig, ax = plt.subplots()
 | 
						|
 | 
						|
    dpi = None
 | 
						|
    codec = None
 | 
						|
    if writer == 'ffmpeg':
 | 
						|
        # Issue #8253
 | 
						|
        fig.set_size_inches((10.85, 9.21))
 | 
						|
        dpi = 100.
 | 
						|
        codec = 'h264'
 | 
						|
 | 
						|
    test_writer = WriterClass()
 | 
						|
    # Use temporary directory for the file-based writers, which produce a file
 | 
						|
    # per frame with known names.
 | 
						|
    with tmpdir.as_cwd():
 | 
						|
        with test_writer.saving(fig, output, dpi):
 | 
						|
            # smoke test it works
 | 
						|
            test_writer.grab_frame()
 | 
						|
            for k in {'dpi', 'bbox_inches', 'format'}:
 | 
						|
                with pytest.raises(
 | 
						|
                        TypeError,
 | 
						|
                        match=f"grab_frame got an unexpected keyword argument {k!r}"
 | 
						|
                ):
 | 
						|
                    test_writer.grab_frame(**{k: object()})
 | 
						|
 | 
						|
 | 
						|
@pytest.mark.parametrize('writer', [
 | 
						|
    pytest.param(
 | 
						|
        'ffmpeg', marks=pytest.mark.skipif(
 | 
						|
            not animation.FFMpegWriter.isAvailable(),
 | 
						|
            reason='Requires FFMpeg')),
 | 
						|
    pytest.param(
 | 
						|
        'imagemagick', marks=pytest.mark.skipif(
 | 
						|
            not animation.ImageMagickWriter.isAvailable(),
 | 
						|
            reason='Requires ImageMagick')),
 | 
						|
])
 | 
						|
@pytest.mark.parametrize('html, want', [
 | 
						|
    ('none', None),
 | 
						|
    ('html5', '<video width'),
 | 
						|
    ('jshtml', '<script ')
 | 
						|
])
 | 
						|
@pytest.mark.parametrize('anim', [dict(klass=dict)], indirect=['anim'])
 | 
						|
def test_animation_repr_html(writer, html, want, anim):
 | 
						|
    if platform.python_implementation() == 'PyPy':
 | 
						|
        # Something in the test setup fixture lingers around into the test and
 | 
						|
        # breaks pytest.warns on PyPy. This garbage collection fixes it.
 | 
						|
        # https://foss.heptapod.net/pypy/pypy/-/issues/3536
 | 
						|
        np.testing.break_cycles()
 | 
						|
    if (writer == 'imagemagick' and html == 'html5'
 | 
						|
            # ImageMagick delegates to ffmpeg for this format.
 | 
						|
            and not animation.FFMpegWriter.isAvailable()):
 | 
						|
        pytest.skip('Requires FFMpeg')
 | 
						|
    # create here rather than in the fixture otherwise we get __del__ warnings
 | 
						|
    # about producing no output
 | 
						|
    anim = animation.FuncAnimation(**anim)
 | 
						|
    with plt.rc_context({'animation.writer': writer,
 | 
						|
                         'animation.html': html}):
 | 
						|
        html = anim._repr_html_()
 | 
						|
    if want is None:
 | 
						|
        assert html is None
 | 
						|
        with pytest.warns(UserWarning):
 | 
						|
            del anim  # Animation was never run, so will warn on cleanup.
 | 
						|
            np.testing.break_cycles()
 | 
						|
    else:
 | 
						|
        assert want in html
 | 
						|
 | 
						|
 | 
						|
@pytest.mark.parametrize(
 | 
						|
    'anim',
 | 
						|
    [{'save_count': 10, 'frames': iter(range(5))}],
 | 
						|
    indirect=['anim']
 | 
						|
)
 | 
						|
def test_no_length_frames(anim):
 | 
						|
    anim.save('unused.null', writer=NullMovieWriter())
 | 
						|
 | 
						|
 | 
						|
def test_movie_writer_registry():
 | 
						|
    assert len(animation.writers._registered) > 0
 | 
						|
    mpl.rcParams['animation.ffmpeg_path'] = "not_available_ever_xxxx"
 | 
						|
    assert not animation.writers.is_available("ffmpeg")
 | 
						|
    # something guaranteed to be available in path and exits immediately
 | 
						|
    bin = "true" if sys.platform != 'win32' else "where"
 | 
						|
    mpl.rcParams['animation.ffmpeg_path'] = bin
 | 
						|
    assert animation.writers.is_available("ffmpeg")
 | 
						|
 | 
						|
 | 
						|
@pytest.mark.parametrize(
 | 
						|
    "method_name",
 | 
						|
    [pytest.param("to_html5_video", marks=pytest.mark.skipif(
 | 
						|
        not animation.writers.is_available(mpl.rcParams["animation.writer"]),
 | 
						|
        reason="animation writer not installed")),
 | 
						|
     "to_jshtml"])
 | 
						|
@pytest.mark.parametrize('anim', [dict(frames=1)], indirect=['anim'])
 | 
						|
def test_embed_limit(method_name, caplog, tmpdir, anim):
 | 
						|
    caplog.set_level("WARNING")
 | 
						|
    with tmpdir.as_cwd():
 | 
						|
        with mpl.rc_context({"animation.embed_limit": 1e-6}):  # ~1 byte.
 | 
						|
            getattr(anim, method_name)()
 | 
						|
    assert len(caplog.records) == 1
 | 
						|
    record, = caplog.records
 | 
						|
    assert (record.name == "matplotlib.animation"
 | 
						|
            and record.levelname == "WARNING")
 | 
						|
 | 
						|
 | 
						|
@pytest.mark.parametrize(
 | 
						|
    "method_name",
 | 
						|
    [pytest.param("to_html5_video", marks=pytest.mark.skipif(
 | 
						|
        not animation.writers.is_available(mpl.rcParams["animation.writer"]),
 | 
						|
        reason="animation writer not installed")),
 | 
						|
     "to_jshtml"])
 | 
						|
@pytest.mark.parametrize('anim', [dict(frames=1)], indirect=['anim'])
 | 
						|
def test_cleanup_temporaries(method_name, tmpdir, anim):
 | 
						|
    with tmpdir.as_cwd():
 | 
						|
        getattr(anim, method_name)()
 | 
						|
        assert list(Path(str(tmpdir)).iterdir()) == []
 | 
						|
 | 
						|
 | 
						|
@pytest.mark.skipif(shutil.which("/bin/sh") is None, reason="requires a POSIX OS")
 | 
						|
def test_failing_ffmpeg(tmpdir, monkeypatch, anim):
 | 
						|
    """
 | 
						|
    Test that we correctly raise a CalledProcessError when ffmpeg fails.
 | 
						|
 | 
						|
    To do so, mock ffmpeg using a simple executable shell script that
 | 
						|
    succeeds when called with no arguments (so that it gets registered by
 | 
						|
    `isAvailable`), but fails otherwise, and add it to the $PATH.
 | 
						|
    """
 | 
						|
    with tmpdir.as_cwd():
 | 
						|
        monkeypatch.setenv("PATH", ".:" + os.environ["PATH"])
 | 
						|
        exe_path = Path(str(tmpdir), "ffmpeg")
 | 
						|
        exe_path.write_bytes(b"#!/bin/sh\n[[ $@ -eq 0 ]]\n")
 | 
						|
        os.chmod(exe_path, 0o755)
 | 
						|
        with pytest.raises(subprocess.CalledProcessError):
 | 
						|
            anim.save("test.mpeg")
 | 
						|
 | 
						|
 | 
						|
@pytest.mark.parametrize("cache_frame_data", [False, True])
 | 
						|
def test_funcanimation_cache_frame_data(cache_frame_data):
 | 
						|
    fig, ax = plt.subplots()
 | 
						|
    line, = ax.plot([], [])
 | 
						|
 | 
						|
    class Frame(dict):
 | 
						|
        # this subclassing enables to use weakref.ref()
 | 
						|
        pass
 | 
						|
 | 
						|
    def init():
 | 
						|
        line.set_data([], [])
 | 
						|
        return line,
 | 
						|
 | 
						|
    def animate(frame):
 | 
						|
        line.set_data(frame['x'], frame['y'])
 | 
						|
        return line,
 | 
						|
 | 
						|
    frames_generated = []
 | 
						|
 | 
						|
    def frames_generator():
 | 
						|
        for _ in range(5):
 | 
						|
            x = np.linspace(0, 10, 100)
 | 
						|
            y = np.random.rand(100)
 | 
						|
 | 
						|
            frame = Frame(x=x, y=y)
 | 
						|
 | 
						|
            # collect weak references to frames
 | 
						|
            # to validate their references later
 | 
						|
            frames_generated.append(weakref.ref(frame))
 | 
						|
 | 
						|
            yield frame
 | 
						|
 | 
						|
    MAX_FRAMES = 100
 | 
						|
    anim = animation.FuncAnimation(fig, animate, init_func=init,
 | 
						|
                                   frames=frames_generator,
 | 
						|
                                   cache_frame_data=cache_frame_data,
 | 
						|
                                   save_count=MAX_FRAMES)
 | 
						|
 | 
						|
    writer = NullMovieWriter()
 | 
						|
    anim.save('unused.null', writer=writer)
 | 
						|
    assert len(frames_generated) == 5
 | 
						|
    np.testing.break_cycles()
 | 
						|
    for f in frames_generated:
 | 
						|
        # If cache_frame_data is True, then the weakref should be alive;
 | 
						|
        # if cache_frame_data is False, then the weakref should be dead (None).
 | 
						|
        assert (f() is None) != cache_frame_data
 | 
						|
 | 
						|
 | 
						|
@pytest.mark.parametrize('return_value', [
 | 
						|
    # User forgot to return (returns None).
 | 
						|
    None,
 | 
						|
    # User returned a string.
 | 
						|
    'string',
 | 
						|
    # User returned an int.
 | 
						|
    1,
 | 
						|
    # User returns a sequence of other objects, e.g., string instead of Artist.
 | 
						|
    ('string', ),
 | 
						|
    # User forgot to return a sequence (handled in `animate` below.)
 | 
						|
    'artist',
 | 
						|
])
 | 
						|
def test_draw_frame(return_value):
 | 
						|
    # test _draw_frame method
 | 
						|
 | 
						|
    fig, ax = plt.subplots()
 | 
						|
    line, = ax.plot([])
 | 
						|
 | 
						|
    def animate(i):
 | 
						|
        # general update func
 | 
						|
        line.set_data([0, 1], [0, i])
 | 
						|
        if return_value == 'artist':
 | 
						|
            # *not* a sequence
 | 
						|
            return line
 | 
						|
        else:
 | 
						|
            return return_value
 | 
						|
 | 
						|
    with pytest.raises(RuntimeError):
 | 
						|
        animation.FuncAnimation(
 | 
						|
            fig, animate, blit=True, cache_frame_data=False
 | 
						|
        )
 | 
						|
 | 
						|
 | 
						|
def test_exhausted_animation(tmpdir):
 | 
						|
    fig, ax = plt.subplots()
 | 
						|
 | 
						|
    def update(frame):
 | 
						|
        return []
 | 
						|
 | 
						|
    anim = animation.FuncAnimation(
 | 
						|
        fig, update, frames=iter(range(10)), repeat=False,
 | 
						|
        cache_frame_data=False
 | 
						|
    )
 | 
						|
 | 
						|
    with tmpdir.as_cwd():
 | 
						|
        anim.save("test.gif", writer='pillow')
 | 
						|
 | 
						|
    with pytest.warns(UserWarning, match="exhausted"):
 | 
						|
        anim._start()
 | 
						|
 | 
						|
 | 
						|
def test_no_frame_warning(tmpdir):
 | 
						|
    fig, ax = plt.subplots()
 | 
						|
 | 
						|
    def update(frame):
 | 
						|
        return []
 | 
						|
 | 
						|
    anim = animation.FuncAnimation(
 | 
						|
        fig, update, frames=[], repeat=False,
 | 
						|
        cache_frame_data=False
 | 
						|
    )
 | 
						|
 | 
						|
    with pytest.warns(UserWarning, match="exhausted"):
 | 
						|
        anim._start()
 | 
						|
 | 
						|
 | 
						|
@check_figures_equal(extensions=["png"])
 | 
						|
def test_animation_frame(tmpdir, fig_test, fig_ref):
 | 
						|
    # Test the expected image after iterating through a few frames
 | 
						|
    # we save the animation to get the iteration because we are not
 | 
						|
    # in an interactive framework.
 | 
						|
    ax = fig_test.add_subplot()
 | 
						|
    ax.set_xlim(0, 2 * np.pi)
 | 
						|
    ax.set_ylim(-1, 1)
 | 
						|
    x = np.linspace(0, 2 * np.pi, 100)
 | 
						|
    line, = ax.plot([], [])
 | 
						|
 | 
						|
    def init():
 | 
						|
        line.set_data([], [])
 | 
						|
        return line,
 | 
						|
 | 
						|
    def animate(i):
 | 
						|
        line.set_data(x, np.sin(x + i / 100))
 | 
						|
        return line,
 | 
						|
 | 
						|
    anim = animation.FuncAnimation(
 | 
						|
        fig_test, animate, init_func=init, frames=5,
 | 
						|
        blit=True, repeat=False)
 | 
						|
    with tmpdir.as_cwd():
 | 
						|
        anim.save("test.gif")
 | 
						|
 | 
						|
    # Reference figure without animation
 | 
						|
    ax = fig_ref.add_subplot()
 | 
						|
    ax.set_xlim(0, 2 * np.pi)
 | 
						|
    ax.set_ylim(-1, 1)
 | 
						|
 | 
						|
    # 5th frame's data
 | 
						|
    ax.plot(x, np.sin(x + 4 / 100))
 | 
						|
 | 
						|
 | 
						|
@pytest.mark.parametrize('anim', [dict(klass=dict)], indirect=['anim'])
 | 
						|
def test_save_count_override_warnings_has_length(anim):
 | 
						|
 | 
						|
    save_count = 5
 | 
						|
    frames = list(range(2))
 | 
						|
    match_target = (
 | 
						|
        f'You passed in an explicit {save_count=} '
 | 
						|
        "which is being ignored in favor of "
 | 
						|
        f"{len(frames)=}."
 | 
						|
    )
 | 
						|
 | 
						|
    with pytest.warns(UserWarning, match=re.escape(match_target)):
 | 
						|
        anim = animation.FuncAnimation(
 | 
						|
            **{**anim, 'frames': frames, 'save_count': save_count}
 | 
						|
        )
 | 
						|
    assert anim._save_count == len(frames)
 | 
						|
    anim._init_draw()
 | 
						|
 | 
						|
 | 
						|
@pytest.mark.parametrize('anim', [dict(klass=dict)], indirect=['anim'])
 | 
						|
def test_save_count_override_warnings_scaler(anim):
 | 
						|
    save_count = 5
 | 
						|
    frames = 7
 | 
						|
    match_target = (
 | 
						|
        f'You passed in an explicit {save_count=} ' +
 | 
						|
        "which is being ignored in favor of " +
 | 
						|
        f"{frames=}."
 | 
						|
    )
 | 
						|
 | 
						|
    with pytest.warns(UserWarning, match=re.escape(match_target)):
 | 
						|
        anim = animation.FuncAnimation(
 | 
						|
            **{**anim, 'frames': frames, 'save_count': save_count}
 | 
						|
        )
 | 
						|
 | 
						|
    assert anim._save_count == frames
 | 
						|
    anim._init_draw()
 | 
						|
 | 
						|
 | 
						|
@pytest.mark.parametrize('anim', [dict(klass=dict)], indirect=['anim'])
 | 
						|
def test_disable_cache_warning(anim):
 | 
						|
    cache_frame_data = True
 | 
						|
    frames = iter(range(5))
 | 
						|
    match_target = (
 | 
						|
        f"{frames=!r} which we can infer the length of, "
 | 
						|
        "did not pass an explicit *save_count* "
 | 
						|
        f"and passed {cache_frame_data=}.  To avoid a possibly "
 | 
						|
        "unbounded cache, frame data caching has been disabled. "
 | 
						|
        "To suppress this warning either pass "
 | 
						|
        "`cache_frame_data=False` or `save_count=MAX_FRAMES`."
 | 
						|
    )
 | 
						|
    with pytest.warns(UserWarning, match=re.escape(match_target)):
 | 
						|
        anim = animation.FuncAnimation(
 | 
						|
            **{**anim, 'cache_frame_data': cache_frame_data, 'frames': frames}
 | 
						|
        )
 | 
						|
    assert anim._cache_frame_data is False
 | 
						|
    anim._init_draw()
 | 
						|
 | 
						|
 | 
						|
def test_movie_writer_invalid_path(anim):
 | 
						|
    if sys.platform == "win32":
 | 
						|
        match_str = r"\[WinError 3] .*'\\\\foo\\\\bar\\\\aardvark'"
 | 
						|
    else:
 | 
						|
        match_str = r"\[Errno 2] .*'/foo"
 | 
						|
    with pytest.raises(FileNotFoundError, match=match_str):
 | 
						|
        anim.save("/foo/bar/aardvark/thiscannotreallyexist.mp4",
 | 
						|
                  writer=animation.FFMpegFileWriter())
 | 
						|
 | 
						|
 | 
						|
def test_animation_with_transparency():
 | 
						|
    """Test animation exhaustion with transparency using PillowWriter directly"""
 | 
						|
    fig, ax = plt.subplots()
 | 
						|
    rect = plt.Rectangle((0, 0), 1, 1, color='red', alpha=0.5)
 | 
						|
    ax.add_patch(rect)
 | 
						|
    ax.set_xlim(0, 1)
 | 
						|
    ax.set_ylim(0, 1)
 | 
						|
 | 
						|
    writer = PillowWriter(fps=30)
 | 
						|
    writer.setup(fig, 'unused.gif', dpi=100)
 | 
						|
    writer.grab_frame(transparent=True)
 | 
						|
    frame = writer._frames[-1]
 | 
						|
    # Check that the alpha channel is not 255, so frame has transparency
 | 
						|
    assert frame.getextrema()[3][0] < 255
 | 
						|
    plt.close(fig)
 |