From 586632b41fa0d79d35b7616fa04f7b847a0095fb Mon Sep 17 00:00:00 2001 From: Julian O Date: Fri, 30 Jun 2017 12:55:15 +1000 Subject: [PATCH 01/32] Exceptions do not have a .message attribute. --- moviepy/config.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/moviepy/config.py b/moviepy/config.py index e102253f8..64035f4fa 100644 --- a/moviepy/config.py +++ b/moviepy/config.py @@ -45,10 +45,9 @@ def try_cmd(cmd): else: success, err = try_cmd([FFMPEG_BINARY]) if not success: - raise IOError(err.message + - "The path specified for the ffmpeg binary might be wrong") - - + raise IOError( + str(err) + + " - The path specified for the ffmpeg binary might be wrong") if IMAGEMAGICK_BINARY=='auto-detect': if os.name == 'nt': @@ -65,8 +64,9 @@ def try_cmd(cmd): else: success, err = try_cmd([IMAGEMAGICK_BINARY]) if not success: - raise IOError(err.message + - "The path specified for the ImageMagick binary might be wrong") + raise IOError( + str(err) + + " - The path specified for the ImageMagick binary might be wrong") From c79d202df9f6b64428dc604c3c6885cbdfe96d7a Mon Sep 17 00:00:00 2001 From: Julian O Date: Fri, 30 Jun 2017 12:58:28 +1000 Subject: [PATCH 02/32] Help tests run on Windows - don't assume temp dir or fonts. * Python already has a feature for finding the temp dir. Changed test helper to take advantage of it. * Still outstanding: Several hard-coded references to /tmp appear in the tests. * Liberation-Mono is not commonly installed on Windows, and even when it is, the font has a different name. Provide a fall-back for Windows fonts. (Considered the use of a 3rd party tool to help select, but seemed overkill.) --- tests/test_PR.py | 9 ++++++--- tests/test_helper.py | 14 ++++++++++++-- tests/test_misc.py | 4 ++-- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/tests/test_PR.py b/tests/test_PR.py index 0c4e2849d..e42c43a33 100644 --- a/tests/test_PR.py +++ b/tests/test_PR.py @@ -9,8 +9,11 @@ from moviepy.video.tools.interpolators import Trajectory from moviepy.video.VideoClip import ColorClip, ImageClip, TextClip + sys.path.append("tests") -from test_helper import TMP_DIR, TRAVIS +from test_helper import TMP_DIR, TRAVIS, FONT + + def test_download_media(capsys): """Test downloading.""" @@ -34,11 +37,11 @@ def test_PR_339(): return # In caption mode. - TextClip(txt='foo', color='white', font="Liberation-Mono", size=(640, 480), + TextClip(txt='foo', color='white', font=FONT, size=(640, 480), method='caption', align='center', fontsize=25) # In label mode. - TextClip(txt='foo', font="Liberation-Mono", method='label') + TextClip(txt='foo', font=FONT, method='label') def test_PR_373(): result = Trajectory.load_list("media/traj.txt") diff --git a/tests/test_helper.py b/tests/test_helper.py index 7913b44e9..d8a9aead9 100644 --- a/tests/test_helper.py +++ b/tests/test_helper.py @@ -2,7 +2,17 @@ """Define general test helper attributes and utilities.""" import os import sys +import tempfile -TRAVIS=os.getenv("TRAVIS_PYTHON_VERSION") is not None +TRAVIS = os.getenv("TRAVIS_PYTHON_VERSION") is not None PYTHON_VERSION = "%s.%s" % (sys.version_info.major, sys.version_info.minor) -TMP_DIR="/tmp" +TMP_DIR = tempfile.tempdir + +# Arbitrary font used in caption testing. +if sys.platform in ("win32", "cygwin"): + FONT = "Helvetica" + # Even if Windows users install the Liberation fonts, it is called LiberationMono on Windows, so + # it doesn't help. +else: + FONT = "Liberation-Mono" # This is available in the fonts-liberation package on Linux. + diff --git a/tests/test_misc.py b/tests/test_misc.py index a375900ad..0ee171801 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -10,7 +10,7 @@ from moviepy.video.io.VideoFileClip import VideoFileClip import download_media -from test_helper import TMP_DIR, TRAVIS +from test_helper import TMP_DIR, TRAVIS, FONT sys.path.append("tests") @@ -35,7 +35,7 @@ def test_subtitles(): if TRAVIS: return - generator = lambda txt: TextClip(txt, font='Liberation-Mono', + generator = lambda txt: TextClip(txt, font=FONT, size=(800,600), fontsize=24, method='caption', align='South', color='white') From 328de3dc9a1524a54ef01b2520bcf08ec627a1f1 Mon Sep 17 00:00:00 2001 From: Julian O Date: Fri, 30 Jun 2017 13:05:02 +1000 Subject: [PATCH 03/32] Help tests run on Windows - allow some flexibility in versions. Building/finding binaries on Windows is non-trivial. Aallow some flexibility in the path levels. (I don't want to force existing users to upgrade, but new users should be allowed the later patches.) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 15f18cc73..4af454d06 100644 --- a/setup.py +++ b/setup.py @@ -61,7 +61,7 @@ def run_tests(self): # Define the requirements for specific execution needs. requires = ['decorator==4.0.11', 'imageio==2.1.2', 'tqdm==4.11.2', 'numpy'] -optional_reqs = ['scikit-image==0.13.0', 'scipy==0.19.0', 'matplotlib==2.0.0'] +optional_reqs = ['scikit-image==0.13.0', 'scipy>=0.19.0,<=0.19.1', 'matplotlib>=2.0.0,<=2.0.2'] documentation_reqs = ['pygame==1.9.3', 'numpydoc>=0.6.0', 'sphinx_rtd_theme>=0.1.10b0', 'Sphinx>=1.5.2'] + optional_reqs test_reqs = ['pytest>=2.8.0', 'nose', 'sklearn', 'pytest-cov', 'coveralls'] \ From 9939f52e64409a7083bd9709c3c87cd9686763b7 Mon Sep 17 00:00:00 2001 From: Julian O Date: Sat, 1 Jul 2017 12:22:48 +1000 Subject: [PATCH 04/32] Issue 596: Add initial support for closing clips. Doesn't do anything yet. The work is done in the subclasses that need it. Also supports context manager, to allow close to be implicitly performed without being forgotten even if an exception occurs during processes. --- moviepy/Clip.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/moviepy/Clip.py b/moviepy/Clip.py index efdfb22c6..59dc5732f 100644 --- a/moviepy/Clip.py +++ b/moviepy/Clip.py @@ -60,7 +60,7 @@ def __init__(self): def copy(self): """ Shallow copy of the clip. - Returns a shwallow copy of the clip whose mask and audio will + Returns a shallow copy of the clip whose mask and audio will be shallow copies of the clip's mask and audio if they exist. This method is intensively used to produce new clips every time @@ -288,6 +288,7 @@ def set_duration(self, t, change_end=True): of the clip. """ self.duration = t + if change_end: self.end = None if (t is None) else (self.start + t) else: @@ -489,3 +490,24 @@ def generator(): return tqdm(generator(), total=nframes) return generator() + + def close(self): + """ + Release any resources that are in use. + + Implementation note for subclasses: + * Memory-based resources can be left to the garbage-collector. + * However, any open files should be closed, and subprocesses should be terminated. + * Be wary that shallow copies are frequently used. Closing a Clip may affect its copies. + * Therefore, should NOT be called by __del__(). + """ + pass + + # Support the Context Manager protocol, to ensure that resources are cleaned up. + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + From e50aef7715052ab028844d6c50ab27a00116e2f7 Mon Sep 17 00:00:00 2001 From: Julian O Date: Sat, 1 Jul 2017 12:27:33 +1000 Subject: [PATCH 05/32] Issue 596: Update doctest examples to call close. Demonstrate good practice in the examples. --- moviepy/video/VideoClip.py | 1 + 1 file changed, 1 insertion(+) diff --git a/moviepy/video/VideoClip.py b/moviepy/video/VideoClip.py index 28caf7d19..6ad8de976 100644 --- a/moviepy/video/VideoClip.py +++ b/moviepy/video/VideoClip.py @@ -250,6 +250,7 @@ def write_videofile(self, filename, fps=None, codec=None, >>> from moviepy.editor import VideoFileClip >>> clip = VideoFileClip("myvideo.mp4").subclip(100,120) >>> clip.write_videofile("my_new_video.mp4") + >>> clip.close() """ name, ext = os.path.splitext(os.path.basename(filename)) From dc4a16afb6c5a7da204e7a63ea7257c8f8a46d6c Mon Sep 17 00:00:00 2001 From: Julian O Date: Sat, 1 Jul 2017 12:29:10 +1000 Subject: [PATCH 06/32] More exception details for easier debugging of ImageMagick issues. Especially for Windows. --- moviepy/config.py | 5 +++-- tests/test_tools.py | 15 ++------------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/moviepy/config.py b/moviepy/config.py index 64035f4fa..5a8451607 100644 --- a/moviepy/config.py +++ b/moviepy/config.py @@ -65,8 +65,9 @@ def try_cmd(cmd): success, err = try_cmd([IMAGEMAGICK_BINARY]) if not success: raise IOError( - str(err) + - " - The path specified for the ImageMagick binary might be wrong") + "%s - The path specified for the ImageMagick binary might be wrong: %s" % + (err, IMAGEMAGICK_BINARY) + ) diff --git a/tests/test_tools.py b/tests/test_tools.py index b1510bb6c..0c5cd9eda 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -2,9 +2,9 @@ """Tool tests meant to be run with pytest. Taken from PR #121 (grimley517).""" import sys import time +import pytest import moviepy.tools as tools -import pytest def test_ext(): @@ -69,17 +69,6 @@ def test_5(): file = sys.stdout.read() assert file == b"" -def test_6(): - """Test subprocess_call for operation. - - The process sleep should run for a given time in seconds. - This checks that the process has deallocated from the stack on - completion of the called process. - - """ - process = tools.subprocess_call(["sleep" , '1']) - time.sleep(1) - assert process is None if __name__ == '__main__': - pytest.main() + pytest.main() From bf9c3ad2483f5d490fd0e51fdfb54a01c3880325 Mon Sep 17 00:00:00 2001 From: Julian O Date: Sat, 1 Jul 2017 12:31:56 +1000 Subject: [PATCH 07/32] Issue #596: Move away from expecting/requiring __del__ to be called. The work should be done in close(). Deleting can be left for the garbage collector. --- moviepy/audio/io/readers.py | 3 ++- moviepy/video/io/VideoFileClip.py | 28 +++++++++++++++++++--------- moviepy/video/io/ffmpeg_reader.py | 13 +++---------- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/moviepy/audio/io/readers.py b/moviepy/audio/io/readers.py index 8e4935ad0..d0bdb6923 100644 --- a/moviepy/audio/io/readers.py +++ b/moviepy/audio/io/readers.py @@ -148,7 +148,7 @@ def close_proc(self): for std in [ self.proc.stdout, self.proc.stderr]: std.close() - del self.proc + self.proc = None def get_frame(self, tt): @@ -245,4 +245,5 @@ def buffer_around(self,framenumber): def __del__(self): + # If the garbage collector comes, make sure the subprocess is terminated. self.close_proc() diff --git a/moviepy/video/io/VideoFileClip.py b/moviepy/video/io/VideoFileClip.py index a44ae2a19..a5e300c40 100644 --- a/moviepy/video/io/VideoFileClip.py +++ b/moviepy/video/io/VideoFileClip.py @@ -12,7 +12,9 @@ class VideoFileClip(VideoClip): A video clip originating from a movie file. For instance: :: >>> clip = VideoFileClip("myHolidays.mp4") - >>> clip2 = VideoFileClip("myMaskVideo.avi") + >>> clip.close() + >>> with VideoFileClip("myMaskVideo.avi") as clip2: + >>> pass # Implicit close called by contex manager. Parameters @@ -61,6 +63,14 @@ class VideoFileClip(VideoClip): Read docs for Clip() and VideoClip() for other, more generic, attributes. + + Lifetime + -------- + + Note that this creates subprocesses and locks files. If you construct one of these instances, you must call + close() afterwards, or the subresources will not be cleaned up until the process ends. + + If copies are made, and close() is called on one, it may cause methods on the other copies to fail. """ @@ -74,7 +84,6 @@ def __init__(self, filename, has_mask=False, # Make a reader pix_fmt= "rgba" if has_mask else "rgb24" - self.reader = None # need this just in case FFMPEG has issues (__del__ complains) self.reader = FFMPEG_VideoReader(filename, pix_fmt=pix_fmt, target_resolution=target_resolution, resize_algo=resize_algorithm, @@ -110,14 +119,15 @@ def __init__(self, filename, has_mask=False, fps = audio_fps, nbytes = audio_nbytes) - def __del__(self): - """ Close/delete the internal reader. """ - try: - del self.reader - except AttributeError: - pass + def close(self): + """ Close the internal reader. """ + if self.reader: + self.reader.close() + self.reader = None try: - del self.audio + if self.audio: + self.audio.close() + self.audio = None except AttributeError: pass diff --git a/moviepy/video/io/ffmpeg_reader.py b/moviepy/video/io/ffmpeg_reader.py index 077c4053f..28b100aa5 100644 --- a/moviepy/video/io/ffmpeg_reader.py +++ b/moviepy/video/io/ffmpeg_reader.py @@ -103,9 +103,6 @@ def initialize(self, starttime=0): self.proc = sp.Popen(cmd, **popen_params) - - - def skip_frames(self, n=1): """Reads and throws away n frames """ w, h = self.size @@ -155,7 +152,7 @@ def get_frame(self, t): Note for coders: getting an arbitrary frame in the video with ffmpeg can be painfully slow if some decoding has to be done. - This function tries to avoid fectching arbitrary frames + This function tries to avoid fetching arbitrary frames whenever possible, by moving between adjacent frames. """ @@ -186,15 +183,11 @@ def close(self): self.proc.stdout.close() self.proc.stderr.close() self.proc.wait() - del self.proc - - def __del__(self): - self.close() - if hasattr(self,'lastread'): + self.proc = None + if hasattr(self, 'lastread'): del self.lastread - def ffmpeg_read_image(filename, with_mask=True): """ Read an image file (PNG, BMP, JPEG...). From de5cb863a4b5b0dcdaaa72055f3ea271acab1e4c Mon Sep 17 00:00:00 2001 From: Julian O Date: Sat, 1 Jul 2017 12:34:05 +1000 Subject: [PATCH 08/32] Issue #596: Move ffmpeg_writer to using close. Again, avoid depending on __del__. Add a context manager interface. Use it lower down. --- moviepy/video/io/ffmpeg_writer.py | 45 ++++++++++++++++++------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/moviepy/video/io/ffmpeg_writer.py b/moviepy/video/io/ffmpeg_writer.py index 9200905f1..f4a2fa6f0 100644 --- a/moviepy/video/io/ffmpeg_writer.py +++ b/moviepy/video/io/ffmpeg_writer.py @@ -122,7 +122,7 @@ def __init__(self, filename, size, fps, codec="libx264", audiofile=None, # This was added so that no extra unwanted window opens on windows # when the child process is created if os.name == "nt": - popen_params["creationflags"] = 0x08000000 + popen_params["creationflags"] = 0x08000000 # CREATE_NO_WINDOW self.proc = sp.Popen(cmd, **popen_params) @@ -178,12 +178,21 @@ def write_frame(self, img_array): raise IOError(error) def close(self): - self.proc.stdin.close() - if self.proc.stderr is not None: - self.proc.stderr.close() - self.proc.wait() + if self.proc: + self.proc.stdin.close() + if self.proc.stderr is not None: + self.proc.stderr.close() + self.proc.wait() - del self.proc + self.proc = None + + # Support the Context Manager protocol, to ensure that resources are cleaned up. + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() def ffmpeg_write_video(clip, filename, fps, codec="libx264", bitrate=None, preset="medium", withmask=False, write_logfile=False, @@ -198,24 +207,22 @@ def ffmpeg_write_video(clip, filename, fps, codec="libx264", bitrate=None, logfile = None verbose_print(verbose, "[MoviePy] Writing video %s\n"%filename) - writer = FFMPEG_VideoWriter(filename, clip.size, fps, codec = codec, + with FFMPEG_VideoWriter(filename, clip.size, fps, codec = codec, preset=preset, bitrate=bitrate, logfile=logfile, audiofile=audiofile, threads=threads, - ffmpeg_params=ffmpeg_params) - - nframes = int(clip.duration*fps) + ffmpeg_params=ffmpeg_params) as writer: - for t,frame in clip.iter_frames(progress_bar=progress_bar, with_times=True, - fps=fps, dtype="uint8"): - if withmask: - mask = (255*clip.mask.get_frame(t)) - if mask.dtype != "uint8": - mask = mask.astype("uint8") - frame = np.dstack([frame,mask]) + nframes = int(clip.duration*fps) - writer.write_frame(frame) + for t,frame in clip.iter_frames(progress_bar=progress_bar, with_times=True, + fps=fps, dtype="uint8"): + if withmask: + mask = (255*clip.mask.get_frame(t)) + if mask.dtype != "uint8": + mask = mask.astype("uint8") + frame = np.dstack([frame,mask]) - writer.close() + writer.write_frame(frame) if write_logfile: logfile.close() From 505939fd34e2847b9fb8d647bb37ffc617a44037 Mon Sep 17 00:00:00 2001 From: Julian O Date: Sat, 1 Jul 2017 12:36:56 +1000 Subject: [PATCH 09/32] Issue #596: Update ffmpeg_audiowriter to support close/context manager. --- moviepy/audio/io/ffmpeg_audiowriter.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/moviepy/audio/io/ffmpeg_audiowriter.py b/moviepy/audio/io/ffmpeg_audiowriter.py index 4e6ca8b71..7ec56be6a 100644 --- a/moviepy/audio/io/ffmpeg_audiowriter.py +++ b/moviepy/audio/io/ffmpeg_audiowriter.py @@ -125,11 +125,27 @@ def write_frames(self,frames_array): def close(self): - self.proc.stdin.close() - if self.proc.stderr is not None: - self.proc.stderr.close() - self.proc.wait() - del self.proc + if self.proc: + self.proc.stdin.close() + self.proc.stdin = None + if self.proc.stderr is not None: + self.proc.stderr.close() + self.proc.stdee = None + # If this causes deadlocks, consider terminating instead. + self.proc.wait() + self.proc = None + + def __del__(self): + # If the garbage collector comes, make sure the subprocess is terminated. + self.close() + + # Support the Context Manager protocol, to ensure that resources are cleaned up. + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() From 09bc06aa223ea30de83c0bc16417c4c8c39c6017 Mon Sep 17 00:00:00 2001 From: Julian O Date: Sat, 1 Jul 2017 12:39:20 +1000 Subject: [PATCH 10/32] Issue #596: Move AudioFileClip to use close(), away from __del__. Was concerned that lambda might include a reference to reader that wasn't cleaned up by close, so changed it over to an equivalent self.reader. Probably has no effect, but feels safer. --- moviepy/audio/io/AudioFileClip.py | 43 ++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/moviepy/audio/io/AudioFileClip.py b/moviepy/audio/io/AudioFileClip.py index 5c9042475..8b86b8920 100644 --- a/moviepy/audio/io/AudioFileClip.py +++ b/moviepy/audio/io/AudioFileClip.py @@ -44,12 +44,27 @@ class AudioFileClip(AudioClip): buffersize See Parameters. + Lifetime + -------- + + Note that this creates subprocesses and locks files. If you construct one of these instances, you must call + close() afterwards, or the subresources will not be cleaned up until the process ends. + + If copies are made, and close() is called on one, it may cause methods on the other copies to fail. + + However, coreaders must be closed separately. + Examples ---------- >>> snd = AudioFileClip("song.wav") + >>> snd.close() >>> snd = AudioFileClip("song.mp3", fps = 44100, bitrate=3000) - >>> snd = AudioFileClip(mySoundArray,fps=44100) # from a numeric array + >>> second_reader = snd.coreader() + >>> second_reader.close() + >>> snd.close() + >>> with AudioFileClip(mySoundArray,fps=44100) as snd: # from a numeric array + >>> pass # Close is implicitly performed by context manager. """ @@ -59,28 +74,26 @@ def __init__(self, filename, buffersize=200000, nbytes=2, fps=44100): AudioClip.__init__(self) self.filename = filename - reader = FFMPEG_AudioReader(filename,fps=fps,nbytes=nbytes, + self.reader = FFMPEG_AudioReader(filename,fps=fps,nbytes=nbytes, buffersize=buffersize) - - self.reader = reader self.fps = fps - self.duration = reader.duration - self.end = reader.duration + self.duration = self.reader.duration + self.end = self.reader.duration - self.make_frame = lambda t: reader.get_frame(t) - self.nchannels = reader.nchannels + self.make_frame = lambda t: self.reader.get_frame(t) + self.nchannels = self.reader.nchannels def coreader(self): """ Returns a copy of the AudioFileClip, i.e. a new entrance point to the audio file. Use copy when you have different clips watching the audio file at different times. """ - return AudioFileClip(self.filename,self.buffersize) + return AudioFileClip(self.filename, self.buffersize) + - def __del__(self): - """ Close/delete the internal reader. """ - try: - del self.reader - except AttributeError: - pass + def close(self): + """ Close the internal reader. """ + if self.reader: + self.reader.close_proc() + self.reader = None From 495cd3ef3cb8362cbef12e400fe45a79438e344b Mon Sep 17 00:00:00 2001 From: Julian O Date: Sat, 1 Jul 2017 12:41:27 +1000 Subject: [PATCH 11/32] Issue #596: Support close() on CompositeVideoClip. Note: It does NOT close all the subclips, because they may be used again (by the caller). It is the caller's job to clean them up. But clips created by this instance are closed by this instance. --- moviepy/video/compositing/CompositeVideoClip.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/moviepy/video/compositing/CompositeVideoClip.py b/moviepy/video/compositing/CompositeVideoClip.py index 30172b782..8d0012ed9 100644 --- a/moviepy/video/compositing/CompositeVideoClip.py +++ b/moviepy/video/compositing/CompositeVideoClip.py @@ -75,9 +75,11 @@ def __init__(self, clips, size=None, bg_color=None, use_bgclip=False, if use_bgclip: self.bg = clips[0] self.clips = clips[1:] + self.created_bg = False else: self.clips = clips self.bg = ColorClip(size, col=self.bg_color) + self.created_bg = True @@ -117,6 +119,18 @@ def playing_clips(self, t=0): actually playing at the given time `t`. """ return [c for c in self.clips if c.is_playing(t)] + def close(self): + if self.created_bg and self.bg: + # Only close the background clip if it was locally created. + # Otherwise, it remains the job of whoever created it. + self.bg.close() + self.bg = None + if hasattr(self, "audio") and self.audio: + self.audio.close() + self.audio = None + + + def clips_array(array, rows_widths=None, cols_widths=None, From 2c5307bb796b944ad7223c41765ea86df965d700 Mon Sep 17 00:00:00 2001 From: Julian O Date: Sat, 1 Jul 2017 12:46:41 +1000 Subject: [PATCH 12/32] Issue #596: Add tests to see if this issue has been repaired. test_resourcereleasedemo exercises the path where close is not called and demonstrates that there is a consistent problem on Windows. Even after this fix, it remains a problem that if you don't call close, moviepg will leak locked files and subprocesses. [Because the problem remains until the process ends, this is included in a separate test file.] test_resourcerelease demonstrates that when close() is called, the problem goes away. --- tests/test_resourcerelease.py | 53 ++++++++++++++++++++++ tests/test_resourcereleasedemo.py | 75 +++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 tests/test_resourcerelease.py create mode 100644 tests/test_resourcereleasedemo.py diff --git a/tests/test_resourcerelease.py b/tests/test_resourcerelease.py new file mode 100644 index 000000000..98c9eb0cd --- /dev/null +++ b/tests/test_resourcerelease.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +""" + Tool tests meant to be run with pytest. + + Testing whether issue #596 has been repaired. + + Note: Platform dependent test. Will only fail on Windows > NT. """ + +from os import remove +from os.path import join +import subprocess as sp +import time +# from tempfile import NamedTemporaryFile +from test_helper import TMP_DIR + +from moviepy.video.compositing.CompositeVideoClip import clips_array +from moviepy.video.VideoClip import ColorClip +from moviepy.video.io.VideoFileClip import VideoFileClip + + +def test_release_of_file_via_close(): + # Create a random video file. + red = ColorClip((1024, 800), color=(255, 0, 0)) + green = ColorClip((1024, 800), color=(0, 255, 0)) + blue = ColorClip((1024, 800), color=(0, 0, 255)) + + red.fps = green.fps = blue.fps = 30 + + # Repeat this so we can see no conflicts. + for i in range(5): + # Get the name of a temporary file we can use. + local_video_filename = join(TMP_DIR, "test_release_of_file_via_close_%s.mp4" % int(time.time())) + + with clips_array([[red, green, blue]]) as ca: + video = ca.set_duration(1) + + video.write_videofile(local_video_filename) + + # Open it up with VideoFileClip. + with VideoFileClip(local_video_filename) as clip: + # Normally a client would do processing here. + pass + + # Now remove the temporary file. + # This would fail on Windows if the file is still locked. + + # This should succeed without exceptions. + remove(local_video_filename) + + red.close() + green.close() + blue.close() diff --git a/tests/test_resourcereleasedemo.py b/tests/test_resourcereleasedemo.py new file mode 100644 index 000000000..51ba6dc6b --- /dev/null +++ b/tests/test_resourcereleasedemo.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- + +""" + Tool tests meant to be run with pytest. + + Demonstrates issue #596 exists. + + Note: Platform dependent test. Will only fail on Windows > NT. """ + +from os import remove +from os.path import join +import subprocess as sp +import time +# from tempfile import NamedTemporaryFile +from test_helper import TMP_DIR + +from moviepy.video.compositing.CompositeVideoClip import clips_array +from moviepy.video.VideoClip import ColorClip +from moviepy.video.io.VideoFileClip import VideoFileClip +# import pytest + +def test_failure_to_release_file(): + + """ This isn't really a test, because it is expected to fail. + It demonstrates that there *is* a problem with not releasing resources when running on + Windows. + + The real issue was that, as of movepy 0.2.3.2, there was no way around it. + + See test_resourcerelease.py to see how the close() methods provide a solution. + """ + + # Get the name of a temporary file we can use. + local_video_filename = join(TMP_DIR, "test_release_of_file_%s.mp4" % int(time.time())) + + # Repeat this so we can see that the problems escalate: + for i in range(5): + + # Create a random video file. + red = ColorClip((1024,800), color=(255,0,0)) + green = ColorClip((1024,800), color=(0,255,0)) + blue = ColorClip((1024,800), color=(0,0,255)) + + red.fps = green.fps = blue.fps = 30 + video = clips_array([[red, green, blue]]).set_duration(1) + + try: + video.write_videofile(local_video_filename) + + # Open it up with VideoFileClip. + clip = VideoFileClip(local_video_filename) + + # Normally a client would do processing here. + + # All finished, so delete the clipS. + del clip + del video + + except IOError: + print("On Windows, this succeeds the first few times around the loop, but eventually fails.") + print("Need to shut down the process now. No more tests in this file.") + return + + + try: + # Now remove the temporary file. + # This will fail on Windows if the file is still locked. + + # In particular, this raises an exception with PermissionError. + # In there was no way to avoid it. + + remove(local_video_filename) + print("You are not running Windows, because that worked.") + except PermissionError: + print("Yes, on Windows this fails.") From c1a1563ef91fd11c72f8deaa900077ae0a93f481 Mon Sep 17 00:00:00 2001 From: Julian O Date: Sat, 1 Jul 2017 12:49:53 +1000 Subject: [PATCH 13/32] Issue #596: Update tests to use close(). * Without tests changes, many of these existing tests do not pass on Windows. --- tests/test_ImageSequenceClip.py | 8 ++--- tests/test_PR.py | 33 +++++++++--------- tests/test_VideoFileClip.py | 40 +++++++++++---------- tests/test_Videos.py | 14 ++++---- tests/test_compositing.py | 39 +++++++++++++-------- tests/test_fx.py | 59 +++++++++++++++---------------- tests/test_issues.py | 61 +++++++++++++++++---------------- tests/test_misc.py | 8 ++--- 8 files changed, 140 insertions(+), 122 deletions(-) diff --git a/tests/test_ImageSequenceClip.py b/tests/test_ImageSequenceClip.py index c188049cd..202c78733 100644 --- a/tests/test_ImageSequenceClip.py +++ b/tests/test_ImageSequenceClip.py @@ -22,9 +22,9 @@ def test_1(): durations.append(i) images.append("media/python_logo_upside_down.png") - clip = ImageSequenceClip(images, durations=durations) - assert clip.duration == sum(durations) - clip.write_videofile("/tmp/ImageSequenceClip1.mp4", fps=30) + with ImageSequenceClip(images, durations=durations) as clip: + assert clip.duration == sum(durations) + clip.write_videofile("/tmp/ImageSequenceClip1.mp4", fps=30) def test_2(): images=[] @@ -37,7 +37,7 @@ def test_2(): #images are not the same size.. with pytest.raises(Exception, message='Expecting Exception'): - ImageSequenceClip(images, durations=durations) + ImageSequenceClip(images, durations=durations).close() if __name__ == '__main__': diff --git a/tests/test_PR.py b/tests/test_PR.py index e42c43a33..f9ab93dca 100644 --- a/tests/test_PR.py +++ b/tests/test_PR.py @@ -38,10 +38,10 @@ def test_PR_339(): # In caption mode. TextClip(txt='foo', color='white', font=FONT, size=(640, 480), - method='caption', align='center', fontsize=25) + method='caption', align='center', fontsize=25).close() # In label mode. - TextClip(txt='foo', font=FONT, method='label') + TextClip(txt='foo', font=FONT, method='label').close() def test_PR_373(): result = Trajectory.load_list("media/traj.txt") @@ -68,16 +68,16 @@ def test_PR_424(): warnings.simplefilter('always') # Alert us of deprecation warnings. # Recommended use - ColorClip([1000, 600], color=(60, 60, 60), duration=10) + ColorClip([1000, 600], color=(60, 60, 60), duration=10).close() with pytest.warns(DeprecationWarning): # Uses `col` so should work the same as above, but give warning. - ColorClip([1000, 600], col=(60, 60, 60), duration=10) + ColorClip([1000, 600], col=(60, 60, 60), duration=10).close() # Catch all warnings as record. with pytest.warns(None) as record: # Should give 2 warnings and use `color`, not `col` - ColorClip([1000, 600], color=(60, 60, 60), duration=10, col=(2,2,2)) + ColorClip([1000, 600], color=(60, 60, 60), duration=10, col=(2,2,2)).close() message1 = 'The `ColorClip` parameter `col` has been deprecated. ' + \ 'Please use `color` instead.' @@ -93,26 +93,27 @@ def test_PR_458(): clip = ColorClip([1000, 600], color=(60, 60, 60), duration=10) clip.write_videofile(os.path.join(TMP_DIR, "test.mp4"), progress_bar=False, fps=30) + clip.close() def test_PR_515(): # Won't actually work until video is in download_media - clip = VideoFileClip("media/fire2.mp4", fps_source='tbr') - assert clip.fps == 90000 - clip = VideoFileClip("media/fire2.mp4", fps_source='fps') - assert clip.fps == 10.51 + with VideoFileClip("media/fire2.mp4", fps_source='tbr') as clip: + assert clip.fps == 90000 + with VideoFileClip("media/fire2.mp4", fps_source='fps') as clip: + assert clip.fps == 10.51 def test_PR_528(): - clip = ImageClip("media/vacation_2017.jpg") - new_clip = scroll(clip, w=1000, x_speed=50) - new_clip = new_clip.set_duration(20) - new_clip.fps = 24 - new_clip.write_videofile(os.path.join(TMP_DIR, "pano.mp4")) + with ImageClip("media/vacation_2017.jpg") as clip: + new_clip = scroll(clip, w=1000, x_speed=50) + new_clip = new_clip.set_duration(20) + new_clip.fps = 24 + new_clip.write_videofile(os.path.join(TMP_DIR, "pano.mp4")) def test_PR_529(): - video_clip = VideoFileClip("media/fire2.mp4") - assert video_clip.rotation == 180 + with VideoFileClip("media/fire2.mp4") as video_clip: + assert video_clip.rotation == 180 if __name__ == '__main__': diff --git a/tests/test_VideoFileClip.py b/tests/test_VideoFileClip.py index a8ec83e2a..61bfd0805 100644 --- a/tests/test_VideoFileClip.py +++ b/tests/test_VideoFileClip.py @@ -15,39 +15,43 @@ def test_setup(): blue = ColorClip((1024,800), color=(0,0,255)) red.fps = green.fps = blue.fps = 30 - video = clips_array([[red, green, blue]]).set_duration(5) - video.write_videofile("/tmp/test.mp4") + with clips_array([[red, green, blue]]).set_duration(5) as video: + video.write_videofile("/tmp/test.mp4") assert os.path.exists("/tmp/test.mp4") - clip = VideoFileClip("/tmp/test.mp4") - assert clip.duration == 5 - assert clip.fps == 30 - assert clip.size == [1024*3, 800] + with VideoFileClip("/tmp/test.mp4") as clip: + assert clip.duration == 5 + assert clip.fps == 30 + assert clip.size == [1024*3, 800] + + red.close() + green.close() + blue.close() def test_ffmpeg_resizing(): """Test FFmpeg resizing, to include downscaling.""" video_file = 'media/big_buck_bunny_432_433.webm' target_resolution = (128, 128) - video = VideoFileClip(video_file, target_resolution=target_resolution) - frame = video.get_frame(0) - assert frame.shape[0:2] == target_resolution + with VideoFileClip(video_file, target_resolution=target_resolution) as video: + frame = video.get_frame(0) + assert frame.shape[0:2] == target_resolution target_resolution = (128, None) - video = VideoFileClip(video_file, target_resolution=target_resolution) - frame = video.get_frame(0) - assert frame.shape[0] == target_resolution[0] + with VideoFileClip(video_file, target_resolution=target_resolution) as video: + frame = video.get_frame(0) + assert frame.shape[0] == target_resolution[0] target_resolution = (None, 128) - video = VideoFileClip(video_file, target_resolution=target_resolution) - frame = video.get_frame(0) - assert frame.shape[1] == target_resolution[1] + with VideoFileClip(video_file, target_resolution=target_resolution) as video: + frame = video.get_frame(0) + assert frame.shape[1] == target_resolution[1] # Test upscaling target_resolution = (None, 2048) - video = VideoFileClip(video_file, target_resolution=target_resolution) - frame = video.get_frame(0) - assert frame.shape[1] == target_resolution[1] + with VideoFileClip(video_file, target_resolution=target_resolution) as video: + frame = video.get_frame(0) + assert frame.shape[1] == target_resolution[1] if __name__ == '__main__': diff --git a/tests/test_Videos.py b/tests/test_Videos.py index bd001a602..f3d17816c 100644 --- a/tests/test_Videos.py +++ b/tests/test_Videos.py @@ -17,15 +17,15 @@ def test_download_media(capsys): download_media.download() def test_afterimage(): - ai = ImageClip("media/afterimage.png") - masked_clip = mask_color(ai, color=[0,255,1]) # for green + with ImageClip("media/afterimage.png") as ai: + masked_clip = mask_color(ai, color=[0,255,1]) # for green - some_background_clip = ColorClip((800,600), color=(255,255,255)) + with ColorClip((800,600), color=(255,255,255)) as some_background_clip: - final_clip = CompositeVideoClip([some_background_clip, masked_clip], - use_bgclip=True) - final_clip.duration = 5 - final_clip.write_videofile("/tmp/afterimage.mp4", fps=30) + with CompositeVideoClip([some_background_clip, masked_clip], + use_bgclip=True) as final_clip: + final_clip.duration = 5 + final_clip.write_videofile("/tmp/afterimage.mp4", fps=30) if __name__ == '__main__': pytest.main() diff --git a/tests/test_compositing.py b/tests/test_compositing.py index 4b2db7a41..fbb47970b 100644 --- a/tests/test_compositing.py +++ b/tests/test_compositing.py @@ -1,7 +1,11 @@ # -*- coding: utf-8 -*- """Compositing tests for use with pytest.""" +from os.path import join +import sys import pytest from moviepy.editor import * +sys.path.append("tests") +from test_helper import TMP_DIR def test_clips_array(): red = ColorClip((1024,800), color=(255,0,0)) @@ -12,25 +16,30 @@ def test_clips_array(): with pytest.raises(ValueError, message="Expecting ValueError (duration not set)"): - video.resize(width=480).write_videofile("/tmp/test_clips_array.mp4") + video.resize(width=480).write_videofile(join(TMP_DIR, "test_clips_array.mp4")) + video.close() + red.close() + green.close() + blue.close() def test_clips_array_duration(): - red = ColorClip((1024,800), color=(255,0,0)) - green = ColorClip((1024,800), color=(0,255,0)) - blue = ColorClip((1024,800), color=(0,0,255)) - - video = clips_array([[red, green, blue]]).set_duration(5) + for i in range(20): + red = ColorClip((1024,800), color=(255,0,0)) + green = ColorClip((1024,800), color=(0,255,0)) + blue = ColorClip((1024,800), color=(0,0,255)) - with pytest.raises(AttributeError, - message="Expecting ValueError (fps not set)"): - video.write_videofile("/tmp/test_clips_array.mp4") + with clips_array([[red, green, blue]]).set_duration(5) as video: + with pytest.raises(AttributeError, + message="Expecting ValueError (fps not set)"): + video.write_videofile(join(TMP_DIR, "test_clips_array.mp4")) - #this one should work correctly - red.fps=green.fps=blue.fps=30 - video = clips_array([[red, green, blue]]).set_duration(5) - video.write_videofile("/tmp/test_clips_array.mp4") + #this one should work correctly + red.fps = green.fps = blue.fps = 30 + with clips_array([[red, green, blue]]).set_duration(5) as video: + video.write_videofile(join(TMP_DIR, "test_clips_array.mp4")) -if __name__ == '__main__': - pytest.main() + red.close() + green.close() + blue.close() diff --git a/tests/test_fx.py b/tests/test_fx.py index 7e400148a..88f5f4280 100644 --- a/tests/test_fx.py +++ b/tests/test_fx.py @@ -10,10 +10,11 @@ from moviepy.video.fx.fadeout import fadeout from moviepy.video.io.VideoFileClip import VideoFileClip +sys.path.append("tests") + import download_media from test_helper import TMP_DIR -sys.path.append("tests") def test_download_media(capsys): @@ -21,51 +22,51 @@ def test_download_media(capsys): download_media.download() def test_blackwhite(): - clip = VideoFileClip("media/big_buck_bunny_432_433.webm") - clip1 = blackwhite(clip) - clip1.write_videofile(os.path.join(TMP_DIR,"blackwhite1.webm")) + with VideoFileClip("media/big_buck_bunny_432_433.webm") as clip: + clip1 = blackwhite(clip) + clip1.write_videofile(os.path.join(TMP_DIR,"blackwhite1.webm")) # This currently fails with a with_mask error! # def test_blink(): -# clip = VideoFileClip("media/big_buck_bunny_0_30.webm").subclip(0,10) -# clip1 = blink(clip, 1, 1) -# clip1.write_videofile(os.path.join(TMP_DIR,"blink1.webm")) +# with VideoFileClip("media/big_buck_bunny_0_30.webm").subclip(0,10) as clip: +# clip1 = blink(clip, 1, 1) +# clip1.write_videofile(os.path.join(TMP_DIR,"blink1.webm")) def test_colorx(): - clip = VideoFileClip("media/big_buck_bunny_432_433.webm") - clip1 = colorx(clip, 2) - clip1.write_videofile(os.path.join(TMP_DIR,"colorx1.webm")) + with VideoFileClip("media/big_buck_bunny_432_433.webm") as clip: + clip1 = colorx(clip, 2) + clip1.write_videofile(os.path.join(TMP_DIR,"colorx1.webm")) def test_crop(): - clip = VideoFileClip("media/big_buck_bunny_432_433.webm") + with VideoFileClip("media/big_buck_bunny_432_433.webm") as clip: - clip1=crop(clip) #ie, no cropping (just tests all default values) - clip1.write_videofile(os.path.join(TMP_DIR, "crop1.webm")) + clip1=crop(clip) #ie, no cropping (just tests all default values) + clip1.write_videofile(os.path.join(TMP_DIR, "crop1.webm")) - clip2=crop(clip, x1=50, y1=60, x2=460, y2=275) - clip2.write_videofile(os.path.join(TMP_DIR, "crop2.webm")) + clip2=crop(clip, x1=50, y1=60, x2=460, y2=275) + clip2.write_videofile(os.path.join(TMP_DIR, "crop2.webm")) - clip3=crop(clip, y1=30) #remove part above y=30 - clip3.write_videofile(os.path.join(TMP_DIR, "crop3.webm")) + clip3=crop(clip, y1=30) #remove part above y=30 + clip3.write_videofile(os.path.join(TMP_DIR, "crop3.webm")) - clip4=crop(clip, x1=10, width=200) # crop a rect that has width=200 - clip4.write_videofile(os.path.join(TMP_DIR, "crop4.webm")) + clip4=crop(clip, x1=10, width=200) # crop a rect that has width=200 + clip4.write_videofile(os.path.join(TMP_DIR, "crop4.webm")) - clip5=crop(clip, x_center=300, y_center=400, width=50, height=150) - clip5.write_videofile(os.path.join(TMP_DIR, "crop5.webm")) + clip5=crop(clip, x_center=300, y_center=400, width=50, height=150) + clip5.write_videofile(os.path.join(TMP_DIR, "crop5.webm")) - clip6=crop(clip, x_center=300, width=400, y1=100, y2=600) - clip6.write_videofile(os.path.join(TMP_DIR, "crop6.webm")) + clip6=crop(clip, x_center=300, width=400, y1=100, y2=600) + clip6.write_videofile(os.path.join(TMP_DIR, "crop6.webm")) def test_fadein(): - clip = VideoFileClip("media/big_buck_bunny_0_30.webm").subclip(0,5) - clip1 = fadein(clip, 1) - clip1.write_videofile(os.path.join(TMP_DIR,"fadein1.webm")) + with VideoFileClip("media/big_buck_bunny_0_30.webm").subclip(0,5) as clip: + clip1 = fadein(clip, 1) + clip1.write_videofile(os.path.join(TMP_DIR,"fadein1.webm")) def test_fadeout(): - clip = VideoFileClip("media/big_buck_bunny_0_30.webm").subclip(0,5) - clip1 = fadeout(clip, 1) - clip1.write_videofile(os.path.join(TMP_DIR,"fadeout1.webm")) + with VideoFileClip("media/big_buck_bunny_0_30.webm").subclip(0,5) as clip: + clip1 = fadeout(clip, 1) + clip1.write_videofile(os.path.join(TMP_DIR,"fadeout1.webm")) if __name__ == '__main__': diff --git a/tests/test_issues.py b/tests/test_issues.py index c125b8faf..1859eb3bb 100644 --- a/tests/test_issues.py +++ b/tests/test_issues.py @@ -18,9 +18,9 @@ def test_download_media(capsys): download_media.download() def test_issue_145(): - video = ColorClip((800, 600), color=(255,0,0)).set_duration(5) - with pytest.raises(Exception, message='Expecting Exception'): - concatenate_videoclips([video], method='composite') + with ColorClip((800, 600), color=(255,0,0)).set_duration(5) as video: + with pytest.raises(Exception, message='Expecting Exception'): + concatenate_videoclips([video], method='composite') def test_issue_190(): #from PIL import Image @@ -40,6 +40,9 @@ def test_issue_285(): ImageClip('media/python_logo.png', duration=10) merged_clip = concatenate_videoclips([clip_1, clip_2, clip_3]) assert merged_clip.duration == 30 + clip_1.close() + clip_2.close() + clip_3.close() def test_issue_334(): last_move = None @@ -126,41 +129,41 @@ def size(t): return (nsw, nsh) return (last_move1[3], last_move1[3] * 1.33) - avatar = VideoFileClip("media/big_buck_bunny_432_433.webm", has_mask=True) - avatar.audio=None - maskclip = ImageClip("media/afterimage.png", ismask=True, transparent=True) - avatar.set_mask(maskclip) #must set maskclip here.. + with VideoFileClip("media/big_buck_bunny_432_433.webm", has_mask=True) as avatar: + avatar.audio=None + maskclip = ImageClip("media/afterimage.png", ismask=True, transparent=True) + avatar.set_mask(maskclip) #must set maskclip here.. - avatar = concatenate_videoclips([avatar]*11) + concatenated = concatenate_videoclips([avatar]*11) - tt = VideoFileClip("media/big_buck_bunny_0_30.webm").subclip(0,11) - # TODO: Setting mask here does not work: .set_mask(maskclip).resize(size)]) - final = CompositeVideoClip([tt, avatar.set_position(posi).resize(size)]) - final.duration = tt.duration - final.write_videofile(os.path.join(TMP_DIR, 'issue_334.mp4'), fps=24) + with VideoFileClip("media/big_buck_bunny_0_30.webm").subclip(0,11) as tt: + # TODO: Setting mask here does not work: .set_mask(maskclip).resize(size)]) + final = CompositeVideoClip([tt, concatenated.set_position(posi).resize(size)]) + final.duration = tt.duration + final.write_videofile(os.path.join(TMP_DIR, 'issue_334.mp4'), fps=24) def test_issue_354(): - clip = ImageClip("media/python_logo.png") + with ImageClip("media/python_logo.png") as clip: - clip.duration = 10 - crosstime = 1 + clip.duration = 10 + crosstime = 1 - # TODO: Should this be removed? - # caption = editor.TextClip("test text", font="Liberation-Sans-Bold", - # color='white', stroke_color='gray', - # stroke_width=2, method='caption', - # size=(1280, 720), fontsize=60, - # align='South-East') - #caption.duration = clip.duration + # TODO: Should this be removed? + # caption = editor.TextClip("test text", font="Liberation-Sans-Bold", + # color='white', stroke_color='gray', + # stroke_width=2, method='caption', + # size=(1280, 720), fontsize=60, + # align='South-East') + #caption.duration = clip.duration - fadecaption = clip.crossfadein(crosstime).crossfadeout(crosstime) - CompositeVideoClip([clip, fadecaption]) + fadecaption = clip.crossfadein(crosstime).crossfadeout(crosstime) + CompositeVideoClip([clip, fadecaption]).close() def test_issue_359(): - video = ColorClip((800, 600), color=(255,0,0)).set_duration(5) - video.fps=30 - video.write_gif(filename=os.path.join(TMP_DIR, "issue_359.gif"), - tempfiles=True) + with ColorClip((800, 600), color=(255,0,0)).set_duration(5) as video: + video.fps=30 + video.write_gif(filename=os.path.join(TMP_DIR, "issue_359.gif"), + tempfiles=True) # TODO: Debug matplotlib failures following successful travis builds. # def test_issue_368(): diff --git a/tests/test_misc.py b/tests/test_misc.py index 0ee171801..7d79e4042 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -20,8 +20,8 @@ def test_download_media(capsys): download_media.download() def test_cuts1(): - clip = VideoFileClip("media/big_buck_bunny_432_433.webm").resize(0.2) - cuts.find_video_period(clip) == pytest.approx(0.966666666667, 0.0001) + with VideoFileClip("media/big_buck_bunny_432_433.webm").resize(0.2) as clip: + cuts.find_video_period(clip) == pytest.approx(0.966666666667, 0.0001) def test_subtitles(): red = ColorClip((800, 600), color=(255,0,0)).set_duration(10) @@ -41,8 +41,8 @@ def test_subtitles(): color='white') subtitles = SubtitlesClip("media/subtitles1.srt", generator) - final = CompositeVideoClip([myvideo, subtitles]) - final.to_videofile(os.path.join(TMP_DIR, "subtitles1.mp4"), fps=30) + with CompositeVideoClip([myvideo, subtitles]) as final: + final.to_videofile(os.path.join(TMP_DIR, "subtitles1.mp4"), fps=30) data = [([0.0, 4.0], 'Red!'), ([5.0, 9.0], 'More Red!'), ([10.0, 14.0], 'Green!'), ([15.0, 19.0], 'More Green!'), From ff38df9a1673e1085f5e8e90fb13b9b451d3a235 Mon Sep 17 00:00:00 2001 From: Julian O Date: Sat, 1 Jul 2017 12:54:57 +1000 Subject: [PATCH 14/32] Further to PR #597: Change to Arial Helvetica wasn't recognised by ImageMagick. Changing to another arbitrary font that should be available on all Windows machines. --- tests/test_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_helper.py b/tests/test_helper.py index d8a9aead9..1dd6a71c1 100644 --- a/tests/test_helper.py +++ b/tests/test_helper.py @@ -10,7 +10,7 @@ # Arbitrary font used in caption testing. if sys.platform in ("win32", "cygwin"): - FONT = "Helvetica" + FONT = "Arial" # Even if Windows users install the Liberation fonts, it is called LiberationMono on Windows, so # it doesn't help. else: From c4d8e40a8b8c4d7c145cf6c2325099802251ce06 Mon Sep 17 00:00:00 2001 From: Julian O Date: Sat, 1 Jul 2017 12:57:21 +1000 Subject: [PATCH 15/32] Issue #596 and #598: Updated test to support close(). Also changed test to meet Issue #598, but that is also being done in PR#585, so will require a merge. --- tests/test_TextClip.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/test_TextClip.py b/tests/test_TextClip.py index b07605c7b..ffd14b462 100644 --- a/tests/test_TextClip.py +++ b/tests/test_TextClip.py @@ -3,9 +3,9 @@ import pytest from moviepy.video.fx.blink import blink from moviepy.video.VideoClip import TextClip -from test_helper import TMP_DIR, TRAVIS sys.path.append("tests") +from test_helper import TMP_DIR, TRAVIS def test_duration(): #TextClip returns the following error under Travis (issue with Imagemagick) @@ -15,12 +15,14 @@ def test_duration(): return clip = TextClip('hello world', size=(1280,720), color='white') - clip.set_duration(5) + clip = clip.set_duration(5) # Changed due to #598. assert clip.duration == 5 + clip.close() clip2 = clip.fx(blink, d_on=1, d_off=1) - clip2.set_duration(5) + clip2 = clip2.set_duration(5) assert clip2.duration == 5 + clip2.close() # Moved from tests.py. Maybe we can remove these? def test_if_textclip_crashes_in_caption_mode(): @@ -28,13 +30,13 @@ def test_if_textclip_crashes_in_caption_mode(): return TextClip(txt='foo', color='white', size=(640, 480), method='caption', - align='center', fontsize=25) + align='center', fontsize=25).close() def test_if_textclip_crashes_in_label_mode(): if TRAVIS: return - TextClip(txt='foo', method='label') + TextClip(txt='foo', method='label').close() if __name__ == '__main__': pytest.main() From e72cc02e7cce17631ecf2b4ea6cc1fe9f7303e6b Mon Sep 17 00:00:00 2001 From: Julian O Date: Sat, 1 Jul 2017 13:06:11 +1000 Subject: [PATCH 16/32] Revert "More exception details for easier debugging of ImageMagick issues." This reverts commit dc4a16afb6c5a7da204e7a63ea7257c8f8a46d6c. I bundled too much into one commit. Reverting and reapplying as two separate commits for better history. --- tests/test_tools.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/test_tools.py b/tests/test_tools.py index 0c5cd9eda..b1510bb6c 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -2,9 +2,9 @@ """Tool tests meant to be run with pytest. Taken from PR #121 (grimley517).""" import sys import time -import pytest import moviepy.tools as tools +import pytest def test_ext(): @@ -69,6 +69,17 @@ def test_5(): file = sys.stdout.read() assert file == b"" +def test_6(): + """Test subprocess_call for operation. + + The process sleep should run for a given time in seconds. + This checks that the process has deallocated from the stack on + completion of the called process. + + """ + process = tools.subprocess_call(["sleep" , '1']) + time.sleep(1) + assert process is None if __name__ == '__main__': - pytest.main() + pytest.main() From ff6688c46b4477334c12aa7068429e8263b39b4c Mon Sep 17 00:00:00 2001 From: Julian O Date: Sat, 1 Jul 2017 13:11:17 +1000 Subject: [PATCH 17/32] Issue #599: test_6 doesn't test anything. Removed as it was crashing on Windows, achieving nothing on Linux. --- tests/test_tools.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/tests/test_tools.py b/tests/test_tools.py index b1510bb6c..2c276b4a4 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -69,17 +69,5 @@ def test_5(): file = sys.stdout.read() assert file == b"" -def test_6(): - """Test subprocess_call for operation. - - The process sleep should run for a given time in seconds. - This checks that the process has deallocated from the stack on - completion of the called process. - - """ - process = tools.subprocess_call(["sleep" , '1']) - time.sleep(1) - assert process is None - if __name__ == '__main__': pytest.main() From f4dcd478169c0e83a9171ab278cb171be4714947 Mon Sep 17 00:00:00 2001 From: Julian O Date: Sun, 2 Jul 2017 03:39:21 +1000 Subject: [PATCH 18/32] Issue #596: Move comment to avoid incorporate into documents. --- moviepy/Clip.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/moviepy/Clip.py b/moviepy/Clip.py index 59dc5732f..69a3ffff4 100644 --- a/moviepy/Clip.py +++ b/moviepy/Clip.py @@ -39,7 +39,7 @@ class Clip: """ - # prefix for all tmeporary video and audio files. + # prefix for all temporary video and audio files. # You can overwrite it with # >>> Clip._TEMP_FILES_PREFIX = "temp_" @@ -180,7 +180,7 @@ def fl_time(self, t_func, apply_to=None, keep_duration=False): -------- >>> # plays the clip (and its mask and sound) twice faster - >>> newclip = clip.fl_time(lambda: 2*t, apply_to=['mask','audio']) + >>> newclip = clip.fl_time(lambda: 2*t, apply_to=['mask', 'audio']) >>> >>> # plays the clip starting at t=3, and backwards: >>> newclip = clip.fl_time(lambda: 3-t) @@ -494,13 +494,15 @@ def generator(): def close(self): """ Release any resources that are in use. - - Implementation note for subclasses: - * Memory-based resources can be left to the garbage-collector. - * However, any open files should be closed, and subprocesses should be terminated. - * Be wary that shallow copies are frequently used. Closing a Clip may affect its copies. - * Therefore, should NOT be called by __del__(). """ + + # Implementation note for subclasses: + # + # * Memory-based resources can be left to the garbage-collector. + # * However, any open files should be closed, and subprocesses should be terminated. + # * Be wary that shallow copies are frequently used. Closing a Clip may affect its copies. + # * Therefore, should NOT be called by __del__(). + pass # Support the Context Manager protocol, to ensure that resources are cleaned up. From 65971a08f7c07f9b82a3ff23c96e33c204e303f8 Mon Sep 17 00:00:00 2001 From: Julian O Date: Sun, 2 Jul 2017 03:40:17 +1000 Subject: [PATCH 19/32] Issue #596: Add usages tips to documentation. --- docs/getting_started/efficient_moviepy.rst | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/getting_started/efficient_moviepy.rst b/docs/getting_started/efficient_moviepy.rst index d05151e64..ea328c307 100644 --- a/docs/getting_started/efficient_moviepy.rst +++ b/docs/getting_started/efficient_moviepy.rst @@ -29,12 +29,38 @@ provides all you need to play around and edit your videos but it will take time .. _previewing: +When to close() a clip +~~~~~~~~~~~~~~~~~~~~~~ + +When you create some types of clip instances - e.g. ``VideoFileClip`` or ``AudioFileClip`` - MoviePy creates a subprocess and locks the file. In order to release those resources when you are finished you should call the ``close()`` method. + +This is more important for more complex applications and it particularly important when running on Windows. While Python's garbage collector should eventually clean it the resources for you, clsing them makes them available earlier. + +However, if you close a clip too early, methods on the clip (and any clips derived from it) become unsafe. + +So, the rules of thumb are: + + * Call ``close()`` on any clip that you **construct** once you have finished using it, and have also finished using any clip that was derived from it. + * Also close any clips you create through ``AudioFileClip.coreader()``. + * Even if you close a ``CompositeVideoClip`` instance, you still need to close the clips it was created from. + * Otherwise, if you have a clip that was created by deriving it from from another clip (e.g. by calling ``set_mask()``), then generally you shouldn't close it. Closing the original clip will also close the copy. + +Clips act as `context managers `_. This means you +can use them with a ``with`` statement, and they will automatically be closed at the end of the block, even if there is +an exception. :: + + with AudioFileClip("song.wav") as clip: + raise NotImplementedError("I will work out how process this song later") + # clip.close() is implicitly called, so the lock on song.wav file is immediately released. + + The many ways of previewing a clip ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When you are editing a video or trying to achieve an effect with MoviePy through a trial and error process, generating the video at each trial can be very long. This section presents a few tricks to go faster. + clip.save_frame """"""""""""""""" From e21be36a7762a4f83269c6ce1b67d1466ad0a379 Mon Sep 17 00:00:00 2001 From: Julian O Date: Sun, 2 Jul 2017 03:41:09 +1000 Subject: [PATCH 20/32] Clip class missing from reference documents. Due to failing import. --- docs/ref/Clip.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ref/Clip.rst b/docs/ref/Clip.rst index 443be07dc..bac02813c 100644 --- a/docs/ref/Clip.rst +++ b/docs/ref/Clip.rst @@ -5,7 +5,7 @@ Clip :class:`Clip` ========================== -.. autoclass:: Clip.Clip +.. autoclass:: moviepy.Clip.Clip :members: :inherited-members: :show-inheritance: From 9383de5d13ac42d66c6bbb7775e5e39732ae8414 Mon Sep 17 00:00:00 2001 From: Julian O Date: Sun, 2 Jul 2017 03:42:04 +1000 Subject: [PATCH 21/32] Copy-edit: Clumsy sentence in documentation. --- docs/install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/install.rst b/docs/install.rst index e5654d560..70f6bef4d 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -44,7 +44,7 @@ For advanced image processing you will need one or several of these packages. Fo - `Scikit Image`_ may be needed for some advanced image manipulation. - `OpenCV 2.4.6`_ or more recent (provides the package ``cv2``) or more recent may be needed for some advanced image manipulation. -If you are on linux, these softwares will surely be in your repos. +If you are on linux, these packages will likely be in your repos. .. _`Numpy`: https://www.scipy.org/install.html .. _Decorator: https://pypi.python.org/pypi/decorator From 7f930e84106c8f639dd578949c292870a8286b82 Mon Sep 17 00:00:00 2001 From: Julian O Date: Sun, 2 Jul 2017 03:43:07 +1000 Subject: [PATCH 22/32] Fix failing doctest. --- moviepy/video/tools/subtitles.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/moviepy/video/tools/subtitles.py b/moviepy/video/tools/subtitles.py index ebd373901..427e2fe86 100644 --- a/moviepy/video/tools/subtitles.py +++ b/moviepy/video/tools/subtitles.py @@ -25,8 +25,7 @@ class SubtitlesClip(VideoClip): >>> from moviepy.video.tools.subtitles import SubtitlesClip >>> from moviepy.video.io.VideoFileClip import VideoFileClip - >>> generator = lambda txt: TextClip(txt, font='Georgia-Regular', - fontsize=24, color='white') + >>> generator = lambda txt: TextClip(txt, font='Georgia-Regular', fontsize=24, color='white') >>> sub = SubtitlesClip("subtitles.srt", generator) >>> myvideo = VideoFileClip("myvideo.avi") >>> final = CompositeVideoClip([clip, subtitles]) From 53c60b46f4ce441c3bf4474394636f121f0e434e Mon Sep 17 00:00:00 2001 From: Julian O Date: Sat, 1 Jul 2017 12:22:48 +1000 Subject: [PATCH 23/32] Issue 596: Add initial support for closing clips. * Add key support for close() * FFMPEG_VideoWriter and FFMPEG_AudioWriter: Support close() and context managers. * Clip: support close() and context manager. Doesn't do anything itself. The work is done in the subclasses that need it. * Clip subclasses: Overrride close. * Move away from depending on clients calling__del__(). Deleting can be left to Garbage Collector. * CompositeVideoClip: Note: Don't close anything that wasn't constructed here. The client needs to be able to control the component clips. * AudioFileClip: Was concerned that lambda might include a reference to reader that wasn't cleaned up by close, so changed it over to an equivalent self.reader. Probably has no effect, but feels safer. * Update tests to use close(). * Note: While many tests pass on Linux either way, a large proportion of the existing unit tests fail on Windows without these changes. * Include changes to many doctest examples - Demonstrate good practice in the examples. * Also, migrate tests to use TEMPDIR where they were not using it. * test_duration(): also corrected a bug in the test (described in #598). This bug is also been addressed in #585, so a merge will be required. * Add two new test files: * test_resourcereleasedemo exercises the path where close is not called and demonstrates that there is a consistent problem on Windows. Even after this fix, it remains a problem that if you don't call close, moviepg will leak locked files and subprocesses. Because the problem remains until the process ends, this is included in a separate test file.] * test_resourcerelease demonstrates that when close() is called, the problem goes away. * Update documentation to include usage tips for close() Not included: * Example code has not been updated to use close(). --- docs/getting_started/efficient_moviepy.rst | 26 +++++++ moviepy/Clip.py | 30 +++++++- moviepy/audio/io/AudioFileClip.py | 43 +++++++---- moviepy/audio/io/ffmpeg_audiowriter.py | 26 +++++-- moviepy/audio/io/readers.py | 3 +- moviepy/video/VideoClip.py | 1 + .../video/compositing/CompositeVideoClip.py | 14 ++++ moviepy/video/io/VideoFileClip.py | 28 ++++--- moviepy/video/io/ffmpeg_reader.py | 13 +--- moviepy/video/io/ffmpeg_writer.py | 45 ++++++----- tests/test_ImageSequenceClip.py | 8 +- tests/test_PR.py | 31 ++++---- tests/test_TextClip.py | 12 +-- tests/test_VideoFileClip.py | 40 +++++----- tests/test_Videos.py | 14 ++-- tests/test_compositing.py | 39 ++++++---- tests/test_fx.py | 59 ++++++++------- tests/test_issues.py | 61 ++++++++------- tests/test_misc.py | 8 +- tests/test_resourcerelease.py | 53 +++++++++++++ tests/test_resourcereleasedemo.py | 75 +++++++++++++++++++ tests/test_tools.py | 15 +--- 22 files changed, 443 insertions(+), 201 deletions(-) create mode 100644 tests/test_resourcerelease.py create mode 100644 tests/test_resourcereleasedemo.py diff --git a/docs/getting_started/efficient_moviepy.rst b/docs/getting_started/efficient_moviepy.rst index d05151e64..ea328c307 100644 --- a/docs/getting_started/efficient_moviepy.rst +++ b/docs/getting_started/efficient_moviepy.rst @@ -29,12 +29,38 @@ provides all you need to play around and edit your videos but it will take time .. _previewing: +When to close() a clip +~~~~~~~~~~~~~~~~~~~~~~ + +When you create some types of clip instances - e.g. ``VideoFileClip`` or ``AudioFileClip`` - MoviePy creates a subprocess and locks the file. In order to release those resources when you are finished you should call the ``close()`` method. + +This is more important for more complex applications and it particularly important when running on Windows. While Python's garbage collector should eventually clean it the resources for you, clsing them makes them available earlier. + +However, if you close a clip too early, methods on the clip (and any clips derived from it) become unsafe. + +So, the rules of thumb are: + + * Call ``close()`` on any clip that you **construct** once you have finished using it, and have also finished using any clip that was derived from it. + * Also close any clips you create through ``AudioFileClip.coreader()``. + * Even if you close a ``CompositeVideoClip`` instance, you still need to close the clips it was created from. + * Otherwise, if you have a clip that was created by deriving it from from another clip (e.g. by calling ``set_mask()``), then generally you shouldn't close it. Closing the original clip will also close the copy. + +Clips act as `context managers `_. This means you +can use them with a ``with`` statement, and they will automatically be closed at the end of the block, even if there is +an exception. :: + + with AudioFileClip("song.wav") as clip: + raise NotImplementedError("I will work out how process this song later") + # clip.close() is implicitly called, so the lock on song.wav file is immediately released. + + The many ways of previewing a clip ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When you are editing a video or trying to achieve an effect with MoviePy through a trial and error process, generating the video at each trial can be very long. This section presents a few tricks to go faster. + clip.save_frame """"""""""""""""" diff --git a/moviepy/Clip.py b/moviepy/Clip.py index efdfb22c6..69a3ffff4 100644 --- a/moviepy/Clip.py +++ b/moviepy/Clip.py @@ -39,7 +39,7 @@ class Clip: """ - # prefix for all tmeporary video and audio files. + # prefix for all temporary video and audio files. # You can overwrite it with # >>> Clip._TEMP_FILES_PREFIX = "temp_" @@ -60,7 +60,7 @@ def __init__(self): def copy(self): """ Shallow copy of the clip. - Returns a shwallow copy of the clip whose mask and audio will + Returns a shallow copy of the clip whose mask and audio will be shallow copies of the clip's mask and audio if they exist. This method is intensively used to produce new clips every time @@ -180,7 +180,7 @@ def fl_time(self, t_func, apply_to=None, keep_duration=False): -------- >>> # plays the clip (and its mask and sound) twice faster - >>> newclip = clip.fl_time(lambda: 2*t, apply_to=['mask','audio']) + >>> newclip = clip.fl_time(lambda: 2*t, apply_to=['mask', 'audio']) >>> >>> # plays the clip starting at t=3, and backwards: >>> newclip = clip.fl_time(lambda: 3-t) @@ -288,6 +288,7 @@ def set_duration(self, t, change_end=True): of the clip. """ self.duration = t + if change_end: self.end = None if (t is None) else (self.start + t) else: @@ -489,3 +490,26 @@ def generator(): return tqdm(generator(), total=nframes) return generator() + + def close(self): + """ + Release any resources that are in use. + """ + + # Implementation note for subclasses: + # + # * Memory-based resources can be left to the garbage-collector. + # * However, any open files should be closed, and subprocesses should be terminated. + # * Be wary that shallow copies are frequently used. Closing a Clip may affect its copies. + # * Therefore, should NOT be called by __del__(). + + pass + + # Support the Context Manager protocol, to ensure that resources are cleaned up. + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + diff --git a/moviepy/audio/io/AudioFileClip.py b/moviepy/audio/io/AudioFileClip.py index 5c9042475..8b86b8920 100644 --- a/moviepy/audio/io/AudioFileClip.py +++ b/moviepy/audio/io/AudioFileClip.py @@ -44,12 +44,27 @@ class AudioFileClip(AudioClip): buffersize See Parameters. + Lifetime + -------- + + Note that this creates subprocesses and locks files. If you construct one of these instances, you must call + close() afterwards, or the subresources will not be cleaned up until the process ends. + + If copies are made, and close() is called on one, it may cause methods on the other copies to fail. + + However, coreaders must be closed separately. + Examples ---------- >>> snd = AudioFileClip("song.wav") + >>> snd.close() >>> snd = AudioFileClip("song.mp3", fps = 44100, bitrate=3000) - >>> snd = AudioFileClip(mySoundArray,fps=44100) # from a numeric array + >>> second_reader = snd.coreader() + >>> second_reader.close() + >>> snd.close() + >>> with AudioFileClip(mySoundArray,fps=44100) as snd: # from a numeric array + >>> pass # Close is implicitly performed by context manager. """ @@ -59,28 +74,26 @@ def __init__(self, filename, buffersize=200000, nbytes=2, fps=44100): AudioClip.__init__(self) self.filename = filename - reader = FFMPEG_AudioReader(filename,fps=fps,nbytes=nbytes, + self.reader = FFMPEG_AudioReader(filename,fps=fps,nbytes=nbytes, buffersize=buffersize) - - self.reader = reader self.fps = fps - self.duration = reader.duration - self.end = reader.duration + self.duration = self.reader.duration + self.end = self.reader.duration - self.make_frame = lambda t: reader.get_frame(t) - self.nchannels = reader.nchannels + self.make_frame = lambda t: self.reader.get_frame(t) + self.nchannels = self.reader.nchannels def coreader(self): """ Returns a copy of the AudioFileClip, i.e. a new entrance point to the audio file. Use copy when you have different clips watching the audio file at different times. """ - return AudioFileClip(self.filename,self.buffersize) + return AudioFileClip(self.filename, self.buffersize) + - def __del__(self): - """ Close/delete the internal reader. """ - try: - del self.reader - except AttributeError: - pass + def close(self): + """ Close the internal reader. """ + if self.reader: + self.reader.close_proc() + self.reader = None diff --git a/moviepy/audio/io/ffmpeg_audiowriter.py b/moviepy/audio/io/ffmpeg_audiowriter.py index 4e6ca8b71..7ec56be6a 100644 --- a/moviepy/audio/io/ffmpeg_audiowriter.py +++ b/moviepy/audio/io/ffmpeg_audiowriter.py @@ -125,11 +125,27 @@ def write_frames(self,frames_array): def close(self): - self.proc.stdin.close() - if self.proc.stderr is not None: - self.proc.stderr.close() - self.proc.wait() - del self.proc + if self.proc: + self.proc.stdin.close() + self.proc.stdin = None + if self.proc.stderr is not None: + self.proc.stderr.close() + self.proc.stdee = None + # If this causes deadlocks, consider terminating instead. + self.proc.wait() + self.proc = None + + def __del__(self): + # If the garbage collector comes, make sure the subprocess is terminated. + self.close() + + # Support the Context Manager protocol, to ensure that resources are cleaned up. + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() diff --git a/moviepy/audio/io/readers.py b/moviepy/audio/io/readers.py index 8e4935ad0..d0bdb6923 100644 --- a/moviepy/audio/io/readers.py +++ b/moviepy/audio/io/readers.py @@ -148,7 +148,7 @@ def close_proc(self): for std in [ self.proc.stdout, self.proc.stderr]: std.close() - del self.proc + self.proc = None def get_frame(self, tt): @@ -245,4 +245,5 @@ def buffer_around(self,framenumber): def __del__(self): + # If the garbage collector comes, make sure the subprocess is terminated. self.close_proc() diff --git a/moviepy/video/VideoClip.py b/moviepy/video/VideoClip.py index 28caf7d19..6ad8de976 100644 --- a/moviepy/video/VideoClip.py +++ b/moviepy/video/VideoClip.py @@ -250,6 +250,7 @@ def write_videofile(self, filename, fps=None, codec=None, >>> from moviepy.editor import VideoFileClip >>> clip = VideoFileClip("myvideo.mp4").subclip(100,120) >>> clip.write_videofile("my_new_video.mp4") + >>> clip.close() """ name, ext = os.path.splitext(os.path.basename(filename)) diff --git a/moviepy/video/compositing/CompositeVideoClip.py b/moviepy/video/compositing/CompositeVideoClip.py index 30172b782..8d0012ed9 100644 --- a/moviepy/video/compositing/CompositeVideoClip.py +++ b/moviepy/video/compositing/CompositeVideoClip.py @@ -75,9 +75,11 @@ def __init__(self, clips, size=None, bg_color=None, use_bgclip=False, if use_bgclip: self.bg = clips[0] self.clips = clips[1:] + self.created_bg = False else: self.clips = clips self.bg = ColorClip(size, col=self.bg_color) + self.created_bg = True @@ -117,6 +119,18 @@ def playing_clips(self, t=0): actually playing at the given time `t`. """ return [c for c in self.clips if c.is_playing(t)] + def close(self): + if self.created_bg and self.bg: + # Only close the background clip if it was locally created. + # Otherwise, it remains the job of whoever created it. + self.bg.close() + self.bg = None + if hasattr(self, "audio") and self.audio: + self.audio.close() + self.audio = None + + + def clips_array(array, rows_widths=None, cols_widths=None, diff --git a/moviepy/video/io/VideoFileClip.py b/moviepy/video/io/VideoFileClip.py index a44ae2a19..a5e300c40 100644 --- a/moviepy/video/io/VideoFileClip.py +++ b/moviepy/video/io/VideoFileClip.py @@ -12,7 +12,9 @@ class VideoFileClip(VideoClip): A video clip originating from a movie file. For instance: :: >>> clip = VideoFileClip("myHolidays.mp4") - >>> clip2 = VideoFileClip("myMaskVideo.avi") + >>> clip.close() + >>> with VideoFileClip("myMaskVideo.avi") as clip2: + >>> pass # Implicit close called by contex manager. Parameters @@ -61,6 +63,14 @@ class VideoFileClip(VideoClip): Read docs for Clip() and VideoClip() for other, more generic, attributes. + + Lifetime + -------- + + Note that this creates subprocesses and locks files. If you construct one of these instances, you must call + close() afterwards, or the subresources will not be cleaned up until the process ends. + + If copies are made, and close() is called on one, it may cause methods on the other copies to fail. """ @@ -74,7 +84,6 @@ def __init__(self, filename, has_mask=False, # Make a reader pix_fmt= "rgba" if has_mask else "rgb24" - self.reader = None # need this just in case FFMPEG has issues (__del__ complains) self.reader = FFMPEG_VideoReader(filename, pix_fmt=pix_fmt, target_resolution=target_resolution, resize_algo=resize_algorithm, @@ -110,14 +119,15 @@ def __init__(self, filename, has_mask=False, fps = audio_fps, nbytes = audio_nbytes) - def __del__(self): - """ Close/delete the internal reader. """ - try: - del self.reader - except AttributeError: - pass + def close(self): + """ Close the internal reader. """ + if self.reader: + self.reader.close() + self.reader = None try: - del self.audio + if self.audio: + self.audio.close() + self.audio = None except AttributeError: pass diff --git a/moviepy/video/io/ffmpeg_reader.py b/moviepy/video/io/ffmpeg_reader.py index 077c4053f..28b100aa5 100644 --- a/moviepy/video/io/ffmpeg_reader.py +++ b/moviepy/video/io/ffmpeg_reader.py @@ -103,9 +103,6 @@ def initialize(self, starttime=0): self.proc = sp.Popen(cmd, **popen_params) - - - def skip_frames(self, n=1): """Reads and throws away n frames """ w, h = self.size @@ -155,7 +152,7 @@ def get_frame(self, t): Note for coders: getting an arbitrary frame in the video with ffmpeg can be painfully slow if some decoding has to be done. - This function tries to avoid fectching arbitrary frames + This function tries to avoid fetching arbitrary frames whenever possible, by moving between adjacent frames. """ @@ -186,15 +183,11 @@ def close(self): self.proc.stdout.close() self.proc.stderr.close() self.proc.wait() - del self.proc - - def __del__(self): - self.close() - if hasattr(self,'lastread'): + self.proc = None + if hasattr(self, 'lastread'): del self.lastread - def ffmpeg_read_image(filename, with_mask=True): """ Read an image file (PNG, BMP, JPEG...). diff --git a/moviepy/video/io/ffmpeg_writer.py b/moviepy/video/io/ffmpeg_writer.py index 9200905f1..f4a2fa6f0 100644 --- a/moviepy/video/io/ffmpeg_writer.py +++ b/moviepy/video/io/ffmpeg_writer.py @@ -122,7 +122,7 @@ def __init__(self, filename, size, fps, codec="libx264", audiofile=None, # This was added so that no extra unwanted window opens on windows # when the child process is created if os.name == "nt": - popen_params["creationflags"] = 0x08000000 + popen_params["creationflags"] = 0x08000000 # CREATE_NO_WINDOW self.proc = sp.Popen(cmd, **popen_params) @@ -178,12 +178,21 @@ def write_frame(self, img_array): raise IOError(error) def close(self): - self.proc.stdin.close() - if self.proc.stderr is not None: - self.proc.stderr.close() - self.proc.wait() + if self.proc: + self.proc.stdin.close() + if self.proc.stderr is not None: + self.proc.stderr.close() + self.proc.wait() - del self.proc + self.proc = None + + # Support the Context Manager protocol, to ensure that resources are cleaned up. + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() def ffmpeg_write_video(clip, filename, fps, codec="libx264", bitrate=None, preset="medium", withmask=False, write_logfile=False, @@ -198,24 +207,22 @@ def ffmpeg_write_video(clip, filename, fps, codec="libx264", bitrate=None, logfile = None verbose_print(verbose, "[MoviePy] Writing video %s\n"%filename) - writer = FFMPEG_VideoWriter(filename, clip.size, fps, codec = codec, + with FFMPEG_VideoWriter(filename, clip.size, fps, codec = codec, preset=preset, bitrate=bitrate, logfile=logfile, audiofile=audiofile, threads=threads, - ffmpeg_params=ffmpeg_params) - - nframes = int(clip.duration*fps) + ffmpeg_params=ffmpeg_params) as writer: - for t,frame in clip.iter_frames(progress_bar=progress_bar, with_times=True, - fps=fps, dtype="uint8"): - if withmask: - mask = (255*clip.mask.get_frame(t)) - if mask.dtype != "uint8": - mask = mask.astype("uint8") - frame = np.dstack([frame,mask]) + nframes = int(clip.duration*fps) - writer.write_frame(frame) + for t,frame in clip.iter_frames(progress_bar=progress_bar, with_times=True, + fps=fps, dtype="uint8"): + if withmask: + mask = (255*clip.mask.get_frame(t)) + if mask.dtype != "uint8": + mask = mask.astype("uint8") + frame = np.dstack([frame,mask]) - writer.close() + writer.write_frame(frame) if write_logfile: logfile.close() diff --git a/tests/test_ImageSequenceClip.py b/tests/test_ImageSequenceClip.py index c188049cd..202c78733 100644 --- a/tests/test_ImageSequenceClip.py +++ b/tests/test_ImageSequenceClip.py @@ -22,9 +22,9 @@ def test_1(): durations.append(i) images.append("media/python_logo_upside_down.png") - clip = ImageSequenceClip(images, durations=durations) - assert clip.duration == sum(durations) - clip.write_videofile("/tmp/ImageSequenceClip1.mp4", fps=30) + with ImageSequenceClip(images, durations=durations) as clip: + assert clip.duration == sum(durations) + clip.write_videofile("/tmp/ImageSequenceClip1.mp4", fps=30) def test_2(): images=[] @@ -37,7 +37,7 @@ def test_2(): #images are not the same size.. with pytest.raises(Exception, message='Expecting Exception'): - ImageSequenceClip(images, durations=durations) + ImageSequenceClip(images, durations=durations).close() if __name__ == '__main__': diff --git a/tests/test_PR.py b/tests/test_PR.py index 0c4e2849d..600ff9ff5 100644 --- a/tests/test_PR.py +++ b/tests/test_PR.py @@ -35,7 +35,7 @@ def test_PR_339(): # In caption mode. TextClip(txt='foo', color='white', font="Liberation-Mono", size=(640, 480), - method='caption', align='center', fontsize=25) + method='caption', align='center', fontsize=25).close() # In label mode. TextClip(txt='foo', font="Liberation-Mono", method='label') @@ -65,16 +65,16 @@ def test_PR_424(): warnings.simplefilter('always') # Alert us of deprecation warnings. # Recommended use - ColorClip([1000, 600], color=(60, 60, 60), duration=10) + ColorClip([1000, 600], color=(60, 60, 60), duration=10).close() with pytest.warns(DeprecationWarning): # Uses `col` so should work the same as above, but give warning. - ColorClip([1000, 600], col=(60, 60, 60), duration=10) + ColorClip([1000, 600], col=(60, 60, 60), duration=10).close() # Catch all warnings as record. with pytest.warns(None) as record: # Should give 2 warnings and use `color`, not `col` - ColorClip([1000, 600], color=(60, 60, 60), duration=10, col=(2,2,2)) + ColorClip([1000, 600], color=(60, 60, 60), duration=10, col=(2,2,2)).close() message1 = 'The `ColorClip` parameter `col` has been deprecated. ' + \ 'Please use `color` instead.' @@ -90,26 +90,27 @@ def test_PR_458(): clip = ColorClip([1000, 600], color=(60, 60, 60), duration=10) clip.write_videofile(os.path.join(TMP_DIR, "test.mp4"), progress_bar=False, fps=30) + clip.close() def test_PR_515(): # Won't actually work until video is in download_media - clip = VideoFileClip("media/fire2.mp4", fps_source='tbr') - assert clip.fps == 90000 - clip = VideoFileClip("media/fire2.mp4", fps_source='fps') - assert clip.fps == 10.51 + with VideoFileClip("media/fire2.mp4", fps_source='tbr') as clip: + assert clip.fps == 90000 + with VideoFileClip("media/fire2.mp4", fps_source='fps') as clip: + assert clip.fps == 10.51 def test_PR_528(): - clip = ImageClip("media/vacation_2017.jpg") - new_clip = scroll(clip, w=1000, x_speed=50) - new_clip = new_clip.set_duration(20) - new_clip.fps = 24 - new_clip.write_videofile(os.path.join(TMP_DIR, "pano.mp4")) + with ImageClip("media/vacation_2017.jpg") as clip: + new_clip = scroll(clip, w=1000, x_speed=50) + new_clip = new_clip.set_duration(20) + new_clip.fps = 24 + new_clip.write_videofile(os.path.join(TMP_DIR, "pano.mp4")) def test_PR_529(): - video_clip = VideoFileClip("media/fire2.mp4") - assert video_clip.rotation == 180 + with VideoFileClip("media/fire2.mp4") as video_clip: + assert video_clip.rotation == 180 if __name__ == '__main__': diff --git a/tests/test_TextClip.py b/tests/test_TextClip.py index b07605c7b..ffd14b462 100644 --- a/tests/test_TextClip.py +++ b/tests/test_TextClip.py @@ -3,9 +3,9 @@ import pytest from moviepy.video.fx.blink import blink from moviepy.video.VideoClip import TextClip -from test_helper import TMP_DIR, TRAVIS sys.path.append("tests") +from test_helper import TMP_DIR, TRAVIS def test_duration(): #TextClip returns the following error under Travis (issue with Imagemagick) @@ -15,12 +15,14 @@ def test_duration(): return clip = TextClip('hello world', size=(1280,720), color='white') - clip.set_duration(5) + clip = clip.set_duration(5) # Changed due to #598. assert clip.duration == 5 + clip.close() clip2 = clip.fx(blink, d_on=1, d_off=1) - clip2.set_duration(5) + clip2 = clip2.set_duration(5) assert clip2.duration == 5 + clip2.close() # Moved from tests.py. Maybe we can remove these? def test_if_textclip_crashes_in_caption_mode(): @@ -28,13 +30,13 @@ def test_if_textclip_crashes_in_caption_mode(): return TextClip(txt='foo', color='white', size=(640, 480), method='caption', - align='center', fontsize=25) + align='center', fontsize=25).close() def test_if_textclip_crashes_in_label_mode(): if TRAVIS: return - TextClip(txt='foo', method='label') + TextClip(txt='foo', method='label').close() if __name__ == '__main__': pytest.main() diff --git a/tests/test_VideoFileClip.py b/tests/test_VideoFileClip.py index a8ec83e2a..61bfd0805 100644 --- a/tests/test_VideoFileClip.py +++ b/tests/test_VideoFileClip.py @@ -15,39 +15,43 @@ def test_setup(): blue = ColorClip((1024,800), color=(0,0,255)) red.fps = green.fps = blue.fps = 30 - video = clips_array([[red, green, blue]]).set_duration(5) - video.write_videofile("/tmp/test.mp4") + with clips_array([[red, green, blue]]).set_duration(5) as video: + video.write_videofile("/tmp/test.mp4") assert os.path.exists("/tmp/test.mp4") - clip = VideoFileClip("/tmp/test.mp4") - assert clip.duration == 5 - assert clip.fps == 30 - assert clip.size == [1024*3, 800] + with VideoFileClip("/tmp/test.mp4") as clip: + assert clip.duration == 5 + assert clip.fps == 30 + assert clip.size == [1024*3, 800] + + red.close() + green.close() + blue.close() def test_ffmpeg_resizing(): """Test FFmpeg resizing, to include downscaling.""" video_file = 'media/big_buck_bunny_432_433.webm' target_resolution = (128, 128) - video = VideoFileClip(video_file, target_resolution=target_resolution) - frame = video.get_frame(0) - assert frame.shape[0:2] == target_resolution + with VideoFileClip(video_file, target_resolution=target_resolution) as video: + frame = video.get_frame(0) + assert frame.shape[0:2] == target_resolution target_resolution = (128, None) - video = VideoFileClip(video_file, target_resolution=target_resolution) - frame = video.get_frame(0) - assert frame.shape[0] == target_resolution[0] + with VideoFileClip(video_file, target_resolution=target_resolution) as video: + frame = video.get_frame(0) + assert frame.shape[0] == target_resolution[0] target_resolution = (None, 128) - video = VideoFileClip(video_file, target_resolution=target_resolution) - frame = video.get_frame(0) - assert frame.shape[1] == target_resolution[1] + with VideoFileClip(video_file, target_resolution=target_resolution) as video: + frame = video.get_frame(0) + assert frame.shape[1] == target_resolution[1] # Test upscaling target_resolution = (None, 2048) - video = VideoFileClip(video_file, target_resolution=target_resolution) - frame = video.get_frame(0) - assert frame.shape[1] == target_resolution[1] + with VideoFileClip(video_file, target_resolution=target_resolution) as video: + frame = video.get_frame(0) + assert frame.shape[1] == target_resolution[1] if __name__ == '__main__': diff --git a/tests/test_Videos.py b/tests/test_Videos.py index bd001a602..f3d17816c 100644 --- a/tests/test_Videos.py +++ b/tests/test_Videos.py @@ -17,15 +17,15 @@ def test_download_media(capsys): download_media.download() def test_afterimage(): - ai = ImageClip("media/afterimage.png") - masked_clip = mask_color(ai, color=[0,255,1]) # for green + with ImageClip("media/afterimage.png") as ai: + masked_clip = mask_color(ai, color=[0,255,1]) # for green - some_background_clip = ColorClip((800,600), color=(255,255,255)) + with ColorClip((800,600), color=(255,255,255)) as some_background_clip: - final_clip = CompositeVideoClip([some_background_clip, masked_clip], - use_bgclip=True) - final_clip.duration = 5 - final_clip.write_videofile("/tmp/afterimage.mp4", fps=30) + with CompositeVideoClip([some_background_clip, masked_clip], + use_bgclip=True) as final_clip: + final_clip.duration = 5 + final_clip.write_videofile("/tmp/afterimage.mp4", fps=30) if __name__ == '__main__': pytest.main() diff --git a/tests/test_compositing.py b/tests/test_compositing.py index 4b2db7a41..fbb47970b 100644 --- a/tests/test_compositing.py +++ b/tests/test_compositing.py @@ -1,7 +1,11 @@ # -*- coding: utf-8 -*- """Compositing tests for use with pytest.""" +from os.path import join +import sys import pytest from moviepy.editor import * +sys.path.append("tests") +from test_helper import TMP_DIR def test_clips_array(): red = ColorClip((1024,800), color=(255,0,0)) @@ -12,25 +16,30 @@ def test_clips_array(): with pytest.raises(ValueError, message="Expecting ValueError (duration not set)"): - video.resize(width=480).write_videofile("/tmp/test_clips_array.mp4") + video.resize(width=480).write_videofile(join(TMP_DIR, "test_clips_array.mp4")) + video.close() + red.close() + green.close() + blue.close() def test_clips_array_duration(): - red = ColorClip((1024,800), color=(255,0,0)) - green = ColorClip((1024,800), color=(0,255,0)) - blue = ColorClip((1024,800), color=(0,0,255)) - - video = clips_array([[red, green, blue]]).set_duration(5) + for i in range(20): + red = ColorClip((1024,800), color=(255,0,0)) + green = ColorClip((1024,800), color=(0,255,0)) + blue = ColorClip((1024,800), color=(0,0,255)) - with pytest.raises(AttributeError, - message="Expecting ValueError (fps not set)"): - video.write_videofile("/tmp/test_clips_array.mp4") + with clips_array([[red, green, blue]]).set_duration(5) as video: + with pytest.raises(AttributeError, + message="Expecting ValueError (fps not set)"): + video.write_videofile(join(TMP_DIR, "test_clips_array.mp4")) - #this one should work correctly - red.fps=green.fps=blue.fps=30 - video = clips_array([[red, green, blue]]).set_duration(5) - video.write_videofile("/tmp/test_clips_array.mp4") + #this one should work correctly + red.fps = green.fps = blue.fps = 30 + with clips_array([[red, green, blue]]).set_duration(5) as video: + video.write_videofile(join(TMP_DIR, "test_clips_array.mp4")) -if __name__ == '__main__': - pytest.main() + red.close() + green.close() + blue.close() diff --git a/tests/test_fx.py b/tests/test_fx.py index 7e400148a..88f5f4280 100644 --- a/tests/test_fx.py +++ b/tests/test_fx.py @@ -10,10 +10,11 @@ from moviepy.video.fx.fadeout import fadeout from moviepy.video.io.VideoFileClip import VideoFileClip +sys.path.append("tests") + import download_media from test_helper import TMP_DIR -sys.path.append("tests") def test_download_media(capsys): @@ -21,51 +22,51 @@ def test_download_media(capsys): download_media.download() def test_blackwhite(): - clip = VideoFileClip("media/big_buck_bunny_432_433.webm") - clip1 = blackwhite(clip) - clip1.write_videofile(os.path.join(TMP_DIR,"blackwhite1.webm")) + with VideoFileClip("media/big_buck_bunny_432_433.webm") as clip: + clip1 = blackwhite(clip) + clip1.write_videofile(os.path.join(TMP_DIR,"blackwhite1.webm")) # This currently fails with a with_mask error! # def test_blink(): -# clip = VideoFileClip("media/big_buck_bunny_0_30.webm").subclip(0,10) -# clip1 = blink(clip, 1, 1) -# clip1.write_videofile(os.path.join(TMP_DIR,"blink1.webm")) +# with VideoFileClip("media/big_buck_bunny_0_30.webm").subclip(0,10) as clip: +# clip1 = blink(clip, 1, 1) +# clip1.write_videofile(os.path.join(TMP_DIR,"blink1.webm")) def test_colorx(): - clip = VideoFileClip("media/big_buck_bunny_432_433.webm") - clip1 = colorx(clip, 2) - clip1.write_videofile(os.path.join(TMP_DIR,"colorx1.webm")) + with VideoFileClip("media/big_buck_bunny_432_433.webm") as clip: + clip1 = colorx(clip, 2) + clip1.write_videofile(os.path.join(TMP_DIR,"colorx1.webm")) def test_crop(): - clip = VideoFileClip("media/big_buck_bunny_432_433.webm") + with VideoFileClip("media/big_buck_bunny_432_433.webm") as clip: - clip1=crop(clip) #ie, no cropping (just tests all default values) - clip1.write_videofile(os.path.join(TMP_DIR, "crop1.webm")) + clip1=crop(clip) #ie, no cropping (just tests all default values) + clip1.write_videofile(os.path.join(TMP_DIR, "crop1.webm")) - clip2=crop(clip, x1=50, y1=60, x2=460, y2=275) - clip2.write_videofile(os.path.join(TMP_DIR, "crop2.webm")) + clip2=crop(clip, x1=50, y1=60, x2=460, y2=275) + clip2.write_videofile(os.path.join(TMP_DIR, "crop2.webm")) - clip3=crop(clip, y1=30) #remove part above y=30 - clip3.write_videofile(os.path.join(TMP_DIR, "crop3.webm")) + clip3=crop(clip, y1=30) #remove part above y=30 + clip3.write_videofile(os.path.join(TMP_DIR, "crop3.webm")) - clip4=crop(clip, x1=10, width=200) # crop a rect that has width=200 - clip4.write_videofile(os.path.join(TMP_DIR, "crop4.webm")) + clip4=crop(clip, x1=10, width=200) # crop a rect that has width=200 + clip4.write_videofile(os.path.join(TMP_DIR, "crop4.webm")) - clip5=crop(clip, x_center=300, y_center=400, width=50, height=150) - clip5.write_videofile(os.path.join(TMP_DIR, "crop5.webm")) + clip5=crop(clip, x_center=300, y_center=400, width=50, height=150) + clip5.write_videofile(os.path.join(TMP_DIR, "crop5.webm")) - clip6=crop(clip, x_center=300, width=400, y1=100, y2=600) - clip6.write_videofile(os.path.join(TMP_DIR, "crop6.webm")) + clip6=crop(clip, x_center=300, width=400, y1=100, y2=600) + clip6.write_videofile(os.path.join(TMP_DIR, "crop6.webm")) def test_fadein(): - clip = VideoFileClip("media/big_buck_bunny_0_30.webm").subclip(0,5) - clip1 = fadein(clip, 1) - clip1.write_videofile(os.path.join(TMP_DIR,"fadein1.webm")) + with VideoFileClip("media/big_buck_bunny_0_30.webm").subclip(0,5) as clip: + clip1 = fadein(clip, 1) + clip1.write_videofile(os.path.join(TMP_DIR,"fadein1.webm")) def test_fadeout(): - clip = VideoFileClip("media/big_buck_bunny_0_30.webm").subclip(0,5) - clip1 = fadeout(clip, 1) - clip1.write_videofile(os.path.join(TMP_DIR,"fadeout1.webm")) + with VideoFileClip("media/big_buck_bunny_0_30.webm").subclip(0,5) as clip: + clip1 = fadeout(clip, 1) + clip1.write_videofile(os.path.join(TMP_DIR,"fadeout1.webm")) if __name__ == '__main__': diff --git a/tests/test_issues.py b/tests/test_issues.py index c125b8faf..1859eb3bb 100644 --- a/tests/test_issues.py +++ b/tests/test_issues.py @@ -18,9 +18,9 @@ def test_download_media(capsys): download_media.download() def test_issue_145(): - video = ColorClip((800, 600), color=(255,0,0)).set_duration(5) - with pytest.raises(Exception, message='Expecting Exception'): - concatenate_videoclips([video], method='composite') + with ColorClip((800, 600), color=(255,0,0)).set_duration(5) as video: + with pytest.raises(Exception, message='Expecting Exception'): + concatenate_videoclips([video], method='composite') def test_issue_190(): #from PIL import Image @@ -40,6 +40,9 @@ def test_issue_285(): ImageClip('media/python_logo.png', duration=10) merged_clip = concatenate_videoclips([clip_1, clip_2, clip_3]) assert merged_clip.duration == 30 + clip_1.close() + clip_2.close() + clip_3.close() def test_issue_334(): last_move = None @@ -126,41 +129,41 @@ def size(t): return (nsw, nsh) return (last_move1[3], last_move1[3] * 1.33) - avatar = VideoFileClip("media/big_buck_bunny_432_433.webm", has_mask=True) - avatar.audio=None - maskclip = ImageClip("media/afterimage.png", ismask=True, transparent=True) - avatar.set_mask(maskclip) #must set maskclip here.. + with VideoFileClip("media/big_buck_bunny_432_433.webm", has_mask=True) as avatar: + avatar.audio=None + maskclip = ImageClip("media/afterimage.png", ismask=True, transparent=True) + avatar.set_mask(maskclip) #must set maskclip here.. - avatar = concatenate_videoclips([avatar]*11) + concatenated = concatenate_videoclips([avatar]*11) - tt = VideoFileClip("media/big_buck_bunny_0_30.webm").subclip(0,11) - # TODO: Setting mask here does not work: .set_mask(maskclip).resize(size)]) - final = CompositeVideoClip([tt, avatar.set_position(posi).resize(size)]) - final.duration = tt.duration - final.write_videofile(os.path.join(TMP_DIR, 'issue_334.mp4'), fps=24) + with VideoFileClip("media/big_buck_bunny_0_30.webm").subclip(0,11) as tt: + # TODO: Setting mask here does not work: .set_mask(maskclip).resize(size)]) + final = CompositeVideoClip([tt, concatenated.set_position(posi).resize(size)]) + final.duration = tt.duration + final.write_videofile(os.path.join(TMP_DIR, 'issue_334.mp4'), fps=24) def test_issue_354(): - clip = ImageClip("media/python_logo.png") + with ImageClip("media/python_logo.png") as clip: - clip.duration = 10 - crosstime = 1 + clip.duration = 10 + crosstime = 1 - # TODO: Should this be removed? - # caption = editor.TextClip("test text", font="Liberation-Sans-Bold", - # color='white', stroke_color='gray', - # stroke_width=2, method='caption', - # size=(1280, 720), fontsize=60, - # align='South-East') - #caption.duration = clip.duration + # TODO: Should this be removed? + # caption = editor.TextClip("test text", font="Liberation-Sans-Bold", + # color='white', stroke_color='gray', + # stroke_width=2, method='caption', + # size=(1280, 720), fontsize=60, + # align='South-East') + #caption.duration = clip.duration - fadecaption = clip.crossfadein(crosstime).crossfadeout(crosstime) - CompositeVideoClip([clip, fadecaption]) + fadecaption = clip.crossfadein(crosstime).crossfadeout(crosstime) + CompositeVideoClip([clip, fadecaption]).close() def test_issue_359(): - video = ColorClip((800, 600), color=(255,0,0)).set_duration(5) - video.fps=30 - video.write_gif(filename=os.path.join(TMP_DIR, "issue_359.gif"), - tempfiles=True) + with ColorClip((800, 600), color=(255,0,0)).set_duration(5) as video: + video.fps=30 + video.write_gif(filename=os.path.join(TMP_DIR, "issue_359.gif"), + tempfiles=True) # TODO: Debug matplotlib failures following successful travis builds. # def test_issue_368(): diff --git a/tests/test_misc.py b/tests/test_misc.py index a375900ad..63b176379 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -20,8 +20,8 @@ def test_download_media(capsys): download_media.download() def test_cuts1(): - clip = VideoFileClip("media/big_buck_bunny_432_433.webm").resize(0.2) - cuts.find_video_period(clip) == pytest.approx(0.966666666667, 0.0001) + with VideoFileClip("media/big_buck_bunny_432_433.webm").resize(0.2) as clip: + cuts.find_video_period(clip) == pytest.approx(0.966666666667, 0.0001) def test_subtitles(): red = ColorClip((800, 600), color=(255,0,0)).set_duration(10) @@ -41,8 +41,8 @@ def test_subtitles(): color='white') subtitles = SubtitlesClip("media/subtitles1.srt", generator) - final = CompositeVideoClip([myvideo, subtitles]) - final.to_videofile(os.path.join(TMP_DIR, "subtitles1.mp4"), fps=30) + with CompositeVideoClip([myvideo, subtitles]) as final: + final.to_videofile(os.path.join(TMP_DIR, "subtitles1.mp4"), fps=30) data = [([0.0, 4.0], 'Red!'), ([5.0, 9.0], 'More Red!'), ([10.0, 14.0], 'Green!'), ([15.0, 19.0], 'More Green!'), diff --git a/tests/test_resourcerelease.py b/tests/test_resourcerelease.py new file mode 100644 index 000000000..98c9eb0cd --- /dev/null +++ b/tests/test_resourcerelease.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +""" + Tool tests meant to be run with pytest. + + Testing whether issue #596 has been repaired. + + Note: Platform dependent test. Will only fail on Windows > NT. """ + +from os import remove +from os.path import join +import subprocess as sp +import time +# from tempfile import NamedTemporaryFile +from test_helper import TMP_DIR + +from moviepy.video.compositing.CompositeVideoClip import clips_array +from moviepy.video.VideoClip import ColorClip +from moviepy.video.io.VideoFileClip import VideoFileClip + + +def test_release_of_file_via_close(): + # Create a random video file. + red = ColorClip((1024, 800), color=(255, 0, 0)) + green = ColorClip((1024, 800), color=(0, 255, 0)) + blue = ColorClip((1024, 800), color=(0, 0, 255)) + + red.fps = green.fps = blue.fps = 30 + + # Repeat this so we can see no conflicts. + for i in range(5): + # Get the name of a temporary file we can use. + local_video_filename = join(TMP_DIR, "test_release_of_file_via_close_%s.mp4" % int(time.time())) + + with clips_array([[red, green, blue]]) as ca: + video = ca.set_duration(1) + + video.write_videofile(local_video_filename) + + # Open it up with VideoFileClip. + with VideoFileClip(local_video_filename) as clip: + # Normally a client would do processing here. + pass + + # Now remove the temporary file. + # This would fail on Windows if the file is still locked. + + # This should succeed without exceptions. + remove(local_video_filename) + + red.close() + green.close() + blue.close() diff --git a/tests/test_resourcereleasedemo.py b/tests/test_resourcereleasedemo.py new file mode 100644 index 000000000..51ba6dc6b --- /dev/null +++ b/tests/test_resourcereleasedemo.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- + +""" + Tool tests meant to be run with pytest. + + Demonstrates issue #596 exists. + + Note: Platform dependent test. Will only fail on Windows > NT. """ + +from os import remove +from os.path import join +import subprocess as sp +import time +# from tempfile import NamedTemporaryFile +from test_helper import TMP_DIR + +from moviepy.video.compositing.CompositeVideoClip import clips_array +from moviepy.video.VideoClip import ColorClip +from moviepy.video.io.VideoFileClip import VideoFileClip +# import pytest + +def test_failure_to_release_file(): + + """ This isn't really a test, because it is expected to fail. + It demonstrates that there *is* a problem with not releasing resources when running on + Windows. + + The real issue was that, as of movepy 0.2.3.2, there was no way around it. + + See test_resourcerelease.py to see how the close() methods provide a solution. + """ + + # Get the name of a temporary file we can use. + local_video_filename = join(TMP_DIR, "test_release_of_file_%s.mp4" % int(time.time())) + + # Repeat this so we can see that the problems escalate: + for i in range(5): + + # Create a random video file. + red = ColorClip((1024,800), color=(255,0,0)) + green = ColorClip((1024,800), color=(0,255,0)) + blue = ColorClip((1024,800), color=(0,0,255)) + + red.fps = green.fps = blue.fps = 30 + video = clips_array([[red, green, blue]]).set_duration(1) + + try: + video.write_videofile(local_video_filename) + + # Open it up with VideoFileClip. + clip = VideoFileClip(local_video_filename) + + # Normally a client would do processing here. + + # All finished, so delete the clipS. + del clip + del video + + except IOError: + print("On Windows, this succeeds the first few times around the loop, but eventually fails.") + print("Need to shut down the process now. No more tests in this file.") + return + + + try: + # Now remove the temporary file. + # This will fail on Windows if the file is still locked. + + # In particular, this raises an exception with PermissionError. + # In there was no way to avoid it. + + remove(local_video_filename) + print("You are not running Windows, because that worked.") + except PermissionError: + print("Yes, on Windows this fails.") diff --git a/tests/test_tools.py b/tests/test_tools.py index b1510bb6c..0c5cd9eda 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -2,9 +2,9 @@ """Tool tests meant to be run with pytest. Taken from PR #121 (grimley517).""" import sys import time +import pytest import moviepy.tools as tools -import pytest def test_ext(): @@ -69,17 +69,6 @@ def test_5(): file = sys.stdout.read() assert file == b"" -def test_6(): - """Test subprocess_call for operation. - - The process sleep should run for a given time in seconds. - This checks that the process has deallocated from the stack on - completion of the called process. - - """ - process = tools.subprocess_call(["sleep" , '1']) - time.sleep(1) - assert process is None if __name__ == '__main__': - pytest.main() + pytest.main() From 2221c5ed75ecd5c9efe405d6bec984263b676974 Mon Sep 17 00:00:00 2001 From: Julian O Date: Sat, 5 Aug 2017 00:14:53 +1000 Subject: [PATCH 24/32] Merge branch 'WindowsSupport' of C:\Users\xboxl\OneDrive\Documents\MyApps\moviepy with conflicts. --- moviepy/video/tools/subtitles.py | 2 +- tests/test_PR.py | 40 +++++++++++++++++--------------- tests/test_helper.py | 14 +++++++++-- tests/test_misc.py | 4 ++-- 4 files changed, 36 insertions(+), 24 deletions(-) diff --git a/moviepy/video/tools/subtitles.py b/moviepy/video/tools/subtitles.py index ebd373901..533a51684 100644 --- a/moviepy/video/tools/subtitles.py +++ b/moviepy/video/tools/subtitles.py @@ -26,7 +26,7 @@ class SubtitlesClip(VideoClip): >>> from moviepy.video.tools.subtitles import SubtitlesClip >>> from moviepy.video.io.VideoFileClip import VideoFileClip >>> generator = lambda txt: TextClip(txt, font='Georgia-Regular', - fontsize=24, color='white') + ... fontsize=24, color='white') >>> sub = SubtitlesClip("subtitles.srt", generator) >>> myvideo = VideoFileClip("myvideo.avi") >>> final = CompositeVideoClip([clip, subtitles]) diff --git a/tests/test_PR.py b/tests/test_PR.py index 600ff9ff5..e42c43a33 100644 --- a/tests/test_PR.py +++ b/tests/test_PR.py @@ -9,8 +9,11 @@ from moviepy.video.tools.interpolators import Trajectory from moviepy.video.VideoClip import ColorClip, ImageClip, TextClip + sys.path.append("tests") -from test_helper import TMP_DIR, TRAVIS +from test_helper import TMP_DIR, TRAVIS, FONT + + def test_download_media(capsys): """Test downloading.""" @@ -34,11 +37,11 @@ def test_PR_339(): return # In caption mode. - TextClip(txt='foo', color='white', font="Liberation-Mono", size=(640, 480), - method='caption', align='center', fontsize=25).close() + TextClip(txt='foo', color='white', font=FONT, size=(640, 480), + method='caption', align='center', fontsize=25) # In label mode. - TextClip(txt='foo', font="Liberation-Mono", method='label') + TextClip(txt='foo', font=FONT, method='label') def test_PR_373(): result = Trajectory.load_list("media/traj.txt") @@ -65,16 +68,16 @@ def test_PR_424(): warnings.simplefilter('always') # Alert us of deprecation warnings. # Recommended use - ColorClip([1000, 600], color=(60, 60, 60), duration=10).close() + ColorClip([1000, 600], color=(60, 60, 60), duration=10) with pytest.warns(DeprecationWarning): # Uses `col` so should work the same as above, but give warning. - ColorClip([1000, 600], col=(60, 60, 60), duration=10).close() + ColorClip([1000, 600], col=(60, 60, 60), duration=10) # Catch all warnings as record. with pytest.warns(None) as record: # Should give 2 warnings and use `color`, not `col` - ColorClip([1000, 600], color=(60, 60, 60), duration=10, col=(2,2,2)).close() + ColorClip([1000, 600], color=(60, 60, 60), duration=10, col=(2,2,2)) message1 = 'The `ColorClip` parameter `col` has been deprecated. ' + \ 'Please use `color` instead.' @@ -90,27 +93,26 @@ def test_PR_458(): clip = ColorClip([1000, 600], color=(60, 60, 60), duration=10) clip.write_videofile(os.path.join(TMP_DIR, "test.mp4"), progress_bar=False, fps=30) - clip.close() def test_PR_515(): # Won't actually work until video is in download_media - with VideoFileClip("media/fire2.mp4", fps_source='tbr') as clip: - assert clip.fps == 90000 - with VideoFileClip("media/fire2.mp4", fps_source='fps') as clip: - assert clip.fps == 10.51 + clip = VideoFileClip("media/fire2.mp4", fps_source='tbr') + assert clip.fps == 90000 + clip = VideoFileClip("media/fire2.mp4", fps_source='fps') + assert clip.fps == 10.51 def test_PR_528(): - with ImageClip("media/vacation_2017.jpg") as clip: - new_clip = scroll(clip, w=1000, x_speed=50) - new_clip = new_clip.set_duration(20) - new_clip.fps = 24 - new_clip.write_videofile(os.path.join(TMP_DIR, "pano.mp4")) + clip = ImageClip("media/vacation_2017.jpg") + new_clip = scroll(clip, w=1000, x_speed=50) + new_clip = new_clip.set_duration(20) + new_clip.fps = 24 + new_clip.write_videofile(os.path.join(TMP_DIR, "pano.mp4")) def test_PR_529(): - with VideoFileClip("media/fire2.mp4") as video_clip: - assert video_clip.rotation == 180 + video_clip = VideoFileClip("media/fire2.mp4") + assert video_clip.rotation == 180 if __name__ == '__main__': diff --git a/tests/test_helper.py b/tests/test_helper.py index 7913b44e9..1dd6a71c1 100644 --- a/tests/test_helper.py +++ b/tests/test_helper.py @@ -2,7 +2,17 @@ """Define general test helper attributes and utilities.""" import os import sys +import tempfile -TRAVIS=os.getenv("TRAVIS_PYTHON_VERSION") is not None +TRAVIS = os.getenv("TRAVIS_PYTHON_VERSION") is not None PYTHON_VERSION = "%s.%s" % (sys.version_info.major, sys.version_info.minor) -TMP_DIR="/tmp" +TMP_DIR = tempfile.tempdir + +# Arbitrary font used in caption testing. +if sys.platform in ("win32", "cygwin"): + FONT = "Arial" + # Even if Windows users install the Liberation fonts, it is called LiberationMono on Windows, so + # it doesn't help. +else: + FONT = "Liberation-Mono" # This is available in the fonts-liberation package on Linux. + diff --git a/tests/test_misc.py b/tests/test_misc.py index 63b176379..7d79e4042 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -10,7 +10,7 @@ from moviepy.video.io.VideoFileClip import VideoFileClip import download_media -from test_helper import TMP_DIR, TRAVIS +from test_helper import TMP_DIR, TRAVIS, FONT sys.path.append("tests") @@ -35,7 +35,7 @@ def test_subtitles(): if TRAVIS: return - generator = lambda txt: TextClip(txt, font='Liberation-Mono', + generator = lambda txt: TextClip(txt, font=FONT, size=(800,600), fontsize=24, method='caption', align='South', color='white') From 0c3b1be436a877c356f42e63d10697dd06dd62d9 Mon Sep 17 00:00:00 2001 From: Julian O Date: Wed, 9 Aug 2017 01:24:49 +1000 Subject: [PATCH 25/32] Neaten up output and PEP8 compliance. Also, make runnable directly (to help debugging) --- tests/download_media.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/download_media.py b/tests/download_media.py index a57428479..89ef7cc19 100644 --- a/tests/download_media.py +++ b/tests/download_media.py @@ -8,9 +8,9 @@ def download_url(url, filename): """Download a file.""" if not os.path.exists(filename): - print('\nDownloading {}\n'.format(filename)) - download_webfile(url, filename) - print('Downloading complete...\n') + print('Downloading {} ...'.format(filename)) + download_webfile(url, filename) + print('Downloading complete.') def download_youtube_video(youtube_id, filename): """Download a video from youtube.""" @@ -35,8 +35,14 @@ def download(): # Loop through download url strings, build out path, and download the asset. for url in urls: _, tail = os.path.split(url) - download_url('{}/{}'.format(github_prefix, url), output.format(tail)) + download_url( + url='{}/{}'.format(github_prefix, url), + filename=output.format(tail)) # Download remaining asset. - download_url('https://data.vision.ee.ethz.ch/cvl/video2gif/kAKZeIzs0Ag.mp4', - 'media/video_with_failing_audio.mp4') + download_url( + url='https://data.vision.ee.ethz.ch/cvl/video2gif/kAKZeIzs0Ag.mp4', + filename='media/video_with_failing_audio.mp4') + +if __name__ == "__main__": + download() \ No newline at end of file From ce44cd76a94239f95cf730a2a05afb5553f150fa Mon Sep 17 00:00:00 2001 From: Julian O Date: Wed, 9 Aug 2017 01:26:17 +1000 Subject: [PATCH 26/32] Remove references to /tmp to allow to run on Windows. --- tests/test_ImageSequenceClip.py | 4 +++- tests/test_VideoFileClip.py | 9 ++++++--- tests/test_Videos.py | 5 +++-- tests/test_issues.py | 6 +++--- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/tests/test_ImageSequenceClip.py b/tests/test_ImageSequenceClip.py index 202c78733..37911be35 100644 --- a/tests/test_ImageSequenceClip.py +++ b/tests/test_ImageSequenceClip.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """Image sequencing clip tests meant to be run with pytest.""" +import os import sys import pytest @@ -7,6 +8,7 @@ sys.path.append("tests") import download_media +from test_helper import TMP_DIR def test_download_media(capsys): with capsys.disabled(): @@ -24,7 +26,7 @@ def test_1(): with ImageSequenceClip(images, durations=durations) as clip: assert clip.duration == sum(durations) - clip.write_videofile("/tmp/ImageSequenceClip1.mp4", fps=30) + clip.write_videofile(os.path.join(TMP_DIR, "ImageSequenceClip1.mp4"), fps=30) def test_2(): images=[] diff --git a/tests/test_VideoFileClip.py b/tests/test_VideoFileClip.py index 61bfd0805..584e5235e 100644 --- a/tests/test_VideoFileClip.py +++ b/tests/test_VideoFileClip.py @@ -1,12 +1,15 @@ # -*- coding: utf-8 -*- """Video file clip tests meant to be run with pytest.""" import os +import sys import pytest from moviepy.video.compositing.CompositeVideoClip import clips_array from moviepy.video.VideoClip import ColorClip from moviepy.video.io.VideoFileClip import VideoFileClip +sys.path.append("tests") +from test_helper import TMP_DIR def test_setup(): """Test VideoFileClip setup.""" @@ -16,11 +19,11 @@ def test_setup(): red.fps = green.fps = blue.fps = 30 with clips_array([[red, green, blue]]).set_duration(5) as video: - video.write_videofile("/tmp/test.mp4") + video.write_videofile(os.path.join(TMP_DIR, "test.mp4")) - assert os.path.exists("/tmp/test.mp4") + assert os.path.exists(os.path.join(TMP_DIR, "test.mp4")) - with VideoFileClip("/tmp/test.mp4") as clip: + with VideoFileClip(os.path.join(TMP_DIR, "test.mp4")) as clip: assert clip.duration == 5 assert clip.fps == 30 assert clip.size == [1024*3, 800] diff --git a/tests/test_Videos.py b/tests/test_Videos.py index f3d17816c..7506c8ee4 100644 --- a/tests/test_Videos.py +++ b/tests/test_Videos.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """Video tests meant to be run with pytest.""" +import os import sys import pytest @@ -10,7 +11,7 @@ import download_media sys.path.append("tests") - +from test_helper import TMP_DIR def test_download_media(capsys): with capsys.disabled(): @@ -25,7 +26,7 @@ def test_afterimage(): with CompositeVideoClip([some_background_clip, masked_clip], use_bgclip=True) as final_clip: final_clip.duration = 5 - final_clip.write_videofile("/tmp/afterimage.mp4", fps=30) + final_clip.write_videofile(os.path.join(TMP_DIR, "afterimage.mp4"), fps=30) if __name__ == '__main__': pytest.main() diff --git a/tests/test_issues.py b/tests/test_issues.py index 1859eb3bb..9c95ba982 100644 --- a/tests/test_issues.py +++ b/tests/test_issues.py @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- """Issue tests meant to be run with pytest.""" import os -#import sys +import sys import pytest from moviepy.editor import * -#sys.path.append("tests") +sys.path.append("tests") import download_media from test_helper import PYTHON_VERSION, TMP_DIR, TRAVIS @@ -256,7 +256,7 @@ def test_issue_470(): subclip = audio_clip.subclip(t_start=6, t_end=9) with pytest.raises(IOError, message="Expecting IOError"): - subclip.write_audiofile('/tmp/issue_470.wav', write_logfile=True) + subclip.write_audiofile(os.path.join(TMP_DIR, 'issue_470.wav'), write_logfile=True) #but this one should work.. subclip = audio_clip.subclip(t_start=6, t_end=8) From 041512305af6bf05319515a8a72652de9eea636f Mon Sep 17 00:00:00 2001 From: Julian O Date: Wed, 9 Aug 2017 01:26:45 +1000 Subject: [PATCH 27/32] Reference to PermissionError failing on Python 2.7. --- tests/test_resourcereleasedemo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_resourcereleasedemo.py b/tests/test_resourcereleasedemo.py index 51ba6dc6b..e190a913a 100644 --- a/tests/test_resourcereleasedemo.py +++ b/tests/test_resourcereleasedemo.py @@ -71,5 +71,5 @@ def test_failure_to_release_file(): remove(local_video_filename) print("You are not running Windows, because that worked.") - except PermissionError: + except OSError: # More specifically, PermissionError in Python 3. print("Yes, on Windows this fails.") From 2fc414f67a0cff74fa9606158666bfa492a36d3e Mon Sep 17 00:00:00 2001 From: Julian O Date: Wed, 9 Aug 2017 01:28:20 +1000 Subject: [PATCH 28/32] Migrate to use requests to avoid certificate problems. Old versions of urlretrieve have old certificates which means one of the video downloads was failing. Also requires changes to setup.py, to come. --- moviepy/video/io/downloader.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/moviepy/video/io/downloader.py b/moviepy/video/io/downloader.py index cd3df06f4..5b579de02 100644 --- a/moviepy/video/io/downloader.py +++ b/moviepy/video/io/downloader.py @@ -4,10 +4,7 @@ import os -try: # Py2 and Py3 compatibility - from urllib import urlretrieve -except: - from urllib.request import urlretrieve +import requests from moviepy.tools import subprocess_call @@ -22,13 +19,16 @@ def download_webfile(url, filename, overwrite=False): return if '.' in url: - urlretrieve(url, filename) + r = requests.get(url, stream=True) + with open(filename, 'wb') as fd: + for chunk in r.iter_content(chunk_size=128): + fd.write(chunk) + else: try: subprocess_call(['youtube-dl', url, '-o', filename]) except OSError as e: - raise OSError(e.message + '\n A possible reason is that youtube-dl' + raise OSError( + e.message + '\n A possible reason is that youtube-dl' ' is not installed on your computer. Install it with ' - ' "pip install youtube-dl"') - - + ' "pip install youtube_dl"') From ec2d5ba531a38170677a0abf6457832f1da5c5fb Mon Sep 17 00:00:00 2001 From: Julian O Date: Wed, 9 Aug 2017 01:31:32 +1000 Subject: [PATCH 29/32] Clean up of dependencies. Including adding ranges, removing unnecessary entries, adding missing entries, adding environment markers, changing versions, and updating pytest parameter handling. --- setup.py | 53 ++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/setup.py b/setup.py index 4af454d06..a00c5bbba 100644 --- a/setup.py +++ b/setup.py @@ -21,12 +21,12 @@ class PyTest(TestCommand): """Handle test execution from setup.""" - user_options = [('pytest-args=', 'a', "Arguments to pass into py.test")] + user_options = [('pytest-args=', 'a', "Arguments to pass into pytest")] def initialize_options(self): """Initialize the PyTest options.""" TestCommand.initialize_options(self) - self.pytest_args = [] + self.pytest_args = "" def finalize_options(self): """Finalize the PyTest options.""" @@ -42,7 +42,7 @@ def run_tests(self): raise ImportError('Running tests requires additional dependencies.' '\nPlease run (pip install moviepy[test])') - errno = pytest.main(self.pytest_args) + errno = pytest.main(self.pytest_args.split(" ")) sys.exit(errno) @@ -57,15 +57,45 @@ def run_tests(self): cmdclass['build_docs'] = BuildDoc +__version__ = None # Explicitly set version to quieten static code checkers. exec(open('moviepy/version.py').read()) # loads __version__ # Define the requirements for specific execution needs. -requires = ['decorator==4.0.11', 'imageio==2.1.2', 'tqdm==4.11.2', 'numpy'] -optional_reqs = ['scikit-image==0.13.0', 'scipy>=0.19.0,<=0.19.1', 'matplotlib>=2.0.0,<=2.0.2'] -documentation_reqs = ['pygame==1.9.3', 'numpydoc>=0.6.0', - 'sphinx_rtd_theme>=0.1.10b0', 'Sphinx>=1.5.2'] + optional_reqs -test_reqs = ['pytest>=2.8.0', 'nose', 'sklearn', 'pytest-cov', 'coveralls'] \ - + optional_reqs +requires = [ + 'decorator>=4.0.2,<5.0', + 'imageio>=2.1.2,<3.0', + 'tqdm>=4.11.2,<5.0', + 'numpy', + ] + +optional_reqs = [ + "opencv-python>=3.0,<4.0; python_version!='2.7'", + "scikit-image>=0.13.0,<1.0; python_version>='3.4'", + "scikit-learn; python_version>='3.4'", + "scipy>=0.19.0,<1.0; python_version!='3.3'", + "matplotlib>=2.0.0,<3.0; python_version>='3.4'", + "youtube_dl" + ] + +doc_reqs = [ + "pygame>=1.9.3,<2.0; python_version!='3.3'", + 'numpydoc>=0.6.0,<1.0', + 'sphinx_rtd_theme>=0.1.10b0,<1.0', + 'Sphinx>=1.5.2,<2.0', + ] + +test_reqs = [ + 'coveralls>=1.1,<2.0', + 'pytest-cov>=2.5.1,<3.0', + 'pytest>=3.0.0,<4.0', + 'requests>=2.8.1,<3.0' + ] + +extra_reqs = { + "optional": optional_reqs, + "doc": doc_reqs, + "test": test_reqs + } # Load the README. with open('README.rst', 'r', 'utf-8') as f: @@ -109,8 +139,5 @@ def run_tests(self): 'release': ('setup.py', __version__)}}, tests_require=test_reqs, install_requires=requires, - extras_require={ - 'optional': optional_reqs, - 'docs': documentation_reqs, - 'test': test_reqs} + extras_require=extra_reqs, ) From fc2b64e4cb42eaac68df632c68da3f2461fc42f8 Mon Sep 17 00:00:00 2001 From: Julian O Date: Wed, 9 Aug 2017 01:33:40 +1000 Subject: [PATCH 30/32] Simplification of Travis file - letting te setup.py do the heavy lifting Remove conditional installations repeating the rules in setup.py Remove some installation of test needs repeating the rules in setup.py Add testing of installation options. --- .travis.yml | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index a6a0d0db0..7976b104e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,23 +8,36 @@ python: - "3.4" - "3.5" - "3.6" -# command to install dependencies + before_install: - sudo add-apt-repository -y ppa:kirillshkrogalev/ffmpeg-next - sudo apt-get -y -qq update - sudo apt-get install -y -qq ffmpeg - mkdir media + + # Ensure PIP is up-to-date to avoid warnings. + - python -m pip install --upgrade pip + # Ensure setuptools is up-to-date to avoid environment_markers bug. + - pip install --upgrade setuptools + # The default py that is installed is too old on some platforms, leading to version conflicts + - pip install --upgrade py pytest + install: - - if [[ $TRAVIS_PYTHON_VERSION == '3.4' || $TRAVIS_PYTHON_VERSION == '3.5' || $TRAVIS_PYTHON_VERSION == '3.6' ]]; then pip install matplotlib; pip install -U scikit-learn; pip install scipy; pip install opencv-python; fi - - if [[ $TRAVIS_PYTHON_VERSION == '2.7' ]]; then pip install scipy; pip install opencv-python; fi - - pip install coveralls - - pip install pytest-cov - - python setup.py install -# command to run tests -before_script: - - py.test tests/ --cov -script: py.test tests/ --doctest-modules -v --cov moviepy --cov-report term-missing + - echo "No install action required. Implicitly performed by the testing." + +# before_script: + +script: + - python setup.py test --pytest-args "tests/ --doctest-modules -v --cov moviepy --cov-report term-missing" + # Now the *code* is tested, let's check that the setup is compatible with PIP without falling over. + - pip install -e . + - pip install -e .[optional] + - pip install -e .[test] + # Only test doc generation on latest. Doesn't work on some earlier versions (3.3), but doesn't matter. + - if [[ $TRAVIS_PYTHON_VERSION == '3.6' ]]; then pip install -e .[doc]; fi + after_success: - coveralls + matrix: fast_finish: true From abcf4ccd637fc5d8fb576be5203f7b64a5304d4a Mon Sep 17 00:00:00 2001 From: Julian O Date: Wed, 9 Aug 2017 01:34:22 +1000 Subject: [PATCH 31/32] Add Appveyor support. --- appveyor.yml | 161 ++++++++++++++++++++++++++++++++++++++ appveyor/run_with_env.cmd | 86 ++++++++++++++++++++ 2 files changed, 247 insertions(+) create mode 100644 appveyor.yml create mode 100644 appveyor/run_with_env.cmd diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 000000000..da3f943ce --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,161 @@ +# This file is used to configure the AppVeyor CI system, for testing on Windows machines. +# +# Code loosely based on https://github.com/ogrisel/python-appveyor-demo +# +# To test with AppVeyor: +# Register on appveyor.com with your GitHub account. +# Create a new appveyor project, using the GitHub details. +# Ideally, configure notifications to post back to GitHub. (Untested) + +environment: + global: + # SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the + # /E:ON and /V:ON options are not enabled in the batch script interpreter + # See: http://stackoverflow.com/a/13751649/163740 + CMD_IN_ENV: "cmd /E:ON /V:ON /C .\\appveyor\\run_with_env.cmd" + + matrix: + + # MoviePy supports Python 2.7 and 3.3 onwards. + # Strategy: + # Test the latest known patch in each version + # Test the oldest and the newest 32 bit release. 64-bit otherwise. + + - PYTHON: "C:\\Python27-x64" + PYTHON_VERSION: "2.7.13" + PYTHON_ARCH: "64" + MINICONDA: C:\Miniconda + CONDA_INSTALL: "numpy" + + - PYTHON: "C:\\Python33-x64" + PYTHON_VERSION: "3.3.5" + PYTHON_ARCH: "64" + MINICONDA: C:\Miniconda3-x64 + CONDA_INSTALL: "numpy" + + - PYTHON: "C:\\Python34-x64" + PYTHON_VERSION: "3.4.5" + PYTHON_ARCH: "64" + MINICONDA: C:\Miniconda3-x64 + CONDA_INSTALL: "numpy" + + - PYTHON: "C:\\Python35-x64" + PYTHON_VERSION: "3.5.3" + PYTHON_ARCH: "64" + MINICONDA: C:\Miniconda35-x64 + CONDA_INSTALL: "numpy" + + - PYTHON: "C:\\Python36-x64" + PYTHON_VERSION: "3.6.2" + PYTHON_ARCH: "64" + MINICONDA: C:\Miniconda36-x64 + CONDA_INSTALL: "numpy" + + - PYTHON: "C:\\Python27" + PYTHON_VERSION: "2.7.13" + PYTHON_ARCH: "32" + MINICONDA: C:\Miniconda + CONDA_INSTALL: "numpy" + + - PYTHON: "C:\\Python34" + PYTHON_VERSION: "3.6.2" + PYTHON_ARCH: "32" + MINICONDA: C:\Miniconda36 + CONDA_INSTALL: "numpy" + +install: + # If there is a newer build queued for the same PR, cancel this one. + # The AppVeyor 'rollout builds' option is supposed to serve the same + # purpose but it is problematic because it tends to cancel builds pushed + # directly to master instead of just PR builds (or the converse). + # credits: JuliaLang developers. + - ps: if ($env:APPVEYOR_PULL_REQUEST_NUMBER -and $env:APPVEYOR_BUILD_NUMBER -ne ((Invoke-RestMethod ` + https://ci.appveyor.com/api/projects/$env:APPVEYOR_ACCOUNT_NAME/$env:APPVEYOR_PROJECT_SLUG/history?recordsNumber=50).builds | ` + Where-Object pullRequestId -eq $env:APPVEYOR_PULL_REQUEST_NUMBER)[0].buildNumber) { ` + throw "There are newer queued builds for this pull request, failing early." } + + # Dump some debugging information about the machine. + # - ECHO "Filesystem root:" + # - ps: "ls \"C:/\"" + # + # - ECHO "Installed SDKs:" + # - ps: "ls \"C:/Program Files/Microsoft SDKs/Windows\"" + # + # - ECHO "Installed projects:" + # - ps: "ls \"C:\\projects\"" + # - ps: "ls \"C:\\projects\\moviepy\"" + + # - ECHO "Environment Variables" + # - set + + + # Prepend desired Python to the PATH of this build (this cannot be + # done from inside the powershell script as it would require to restart + # the parent CMD process). + - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" + + # Prepare Miniconda. + - "ECHO Miniconda is installed in %MINICONDA%, and will be used to install %CONDA_INSTALL%" + + - "set PATH=%MINICONDA%;%MINICONDA%\\Scripts;%PATH%" + - conda config --set always_yes yes --set changeps1 no + - conda update -q conda + + # Avoid warning from conda info. + - conda install -q -n root _license + # Dump the setup for debugging. + - conda info -a + + # PIP finds some packages challenging. Let Miniconda install them. + - conda create --verbose -q -n test-environment python=%PYTHON_VERSION% %CONDA_INSTALL% + - activate test-environment + + # Upgrade to the latest version of pip to avoid it displaying warnings + # about it being out of date. + - pip install --disable-pip-version-check --user --upgrade pip + - pip install --user --upgrade setuptools + + + # Install ImageMagick (which also installs ffmpeg.) + # This installation process is a big fragile, as new releases are issued, but no Conda package exists yet. + - "ECHO Downloading ImageMagick" + # Versions >=7.0 have problems - executables changed names. + # Assume 64-bit. Need to change to x86 for 32-bit. + # The available version at this site changes - each time it needs to be corrected in four places + # in the next few lines. + - curl -fskLO ftp://ftp.fifi.org/pub/ImageMagick/binaries/ImageMagick-6.9.9-5-Q16-x64-static.exe + - "ECHO Installing ImageMagick" + - "ImageMagick-6.9.9-5-Q16-x64-static.exe /verySILENT /SP" + - set IMAGEMAGICK_BINARY=c:\\Program Files\\ImageMagick-6.9.9-Q16\\convert.exe + - set FFMPEG_BINARY=c:\\Program Files\\ImageMagick-6.9.9-Q16\\ffmpeg.exe + + # Check that we have the expected set-up. + - "ECHO We specified %PYTHON_VERSION% win%PYTHON_ARCH%" + - "python --version" + - "python -c \"import struct; print('Architecture is win'+str(struct.calcsize('P') * 8))\"" + +build_script: + + # Build the compiled extension + - "%CMD_IN_ENV% python c:\\projects\\moviepy\\setup.py build" + +test_script: + # Run the project tests + - "%CMD_IN_ENV% python c:\\projects\\moviepy\\setup.py test" + +# TODO: Support the post-test generation of binaries - Pending a version number that is supported (e.g. 0.3.0) +# +# after_test: +# +# # If tests are successful, create binary packages for the project. +# - "%CMD_IN_ENV% python c:\\projects\\moviepy\\setup.py bdist_wheel" +# - "%CMD_IN_ENV% python c:\\projects\\moviepy\\setup.py bdist_wininst" +# - "%CMD_IN_ENV% python c:\\projects\\moviepy\\setup.py bdist_msi" +# - ps: "ls dist" +# +# artifacts: +# # Archive the generated packages in the ci.appveyor.com build report. +# - path: dist\* +# +# on_success: +# - TODO: upload the content of dist/*.whl to a public wheelhouse diff --git a/appveyor/run_with_env.cmd b/appveyor/run_with_env.cmd new file mode 100644 index 000000000..87c8761e1 --- /dev/null +++ b/appveyor/run_with_env.cmd @@ -0,0 +1,86 @@ +:: To build extensions for 64 bit Python 3, we need to configure environment +:: variables to use the MSVC 2010 C++ compilers from GRMSDKX_EN_DVD.iso of: +:: MS Windows SDK for Windows 7 and .NET Framework 4 (SDK v7.1) +:: +:: To build extensions for 64 bit Python 2, we need to configure environment +:: variables to use the MSVC 2008 C++ compilers from GRMSDKX_EN_DVD.iso of: +:: MS Windows SDK for Windows 7 and .NET Framework 3.5 (SDK v7.0) +:: +:: 32 bit builds, and 64-bit builds for 3.5 and beyond, do not require specific +:: environment configurations. +:: +:: Note: this script needs to be run with the /E:ON and /V:ON flags for the +:: cmd interpreter, at least for (SDK v7.0) +:: +:: More details at: +:: https://github.com/cython/cython/wiki/64BitCythonExtensionsOnWindows +:: http://stackoverflow.com/a/13751649/163740 +:: +:: Author: Olivier Grisel +:: License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/ +:: +:: Notes about batch files for Python people: +:: +:: Quotes in values are literally part of the values: +:: SET FOO="bar" +:: FOO is now five characters long: " b a r " +:: If you don't want quotes, don't include them on the right-hand side. +:: +:: The CALL lines at the end of this file look redundant, but if you move them +:: outside of the IF clauses, they do not run properly in the SET_SDK_64==Y +:: case, I don't know why. +@ECHO OFF + +SET COMMAND_TO_RUN=%* +SET WIN_SDK_ROOT=C:\Program Files\Microsoft SDKs\Windows +SET WIN_WDK=c:\Program Files (x86)\Windows Kits\10\Include\wdf + +:: Extract the major and minor versions, and allow for the minor version to be +:: more than 9. This requires the version number to have two dots in it. +SET MAJOR_PYTHON_VERSION=%PYTHON_VERSION:~0,1% +IF "%PYTHON_VERSION:~3,1%" == "." ( + SET MINOR_PYTHON_VERSION=%PYTHON_VERSION:~2,1% +) ELSE ( + SET MINOR_PYTHON_VERSION=%PYTHON_VERSION:~2,2% +) +:: Based on the Python version, determine what SDK version to use, and whether +:: to set the SDK for 64-bit. +IF %MAJOR_PYTHON_VERSION% == 2 ( + SET WINDOWS_SDK_VERSION="v7.0" + SET SET_SDK_64=Y +) ELSE ( + IF %MAJOR_PYTHON_VERSION% == 3 ( + SET WINDOWS_SDK_VERSION="v7.1" + IF %MINOR_PYTHON_VERSION% LEQ 4 ( + SET SET_SDK_64=Y + ) ELSE ( + SET SET_SDK_64=N + IF EXIST "%WIN_WDK%" ( + :: See: https://connect.microsoft.com/VisualStudio/feedback/details/1610302/ + REN "%WIN_WDK%" 0wdf + ) + ) + ) ELSE ( + ECHO Unsupported Python version: "%MAJOR_PYTHON_VERSION%" + EXIT 1 + ) +) +IF %PYTHON_ARCH% == 64 ( + IF %SET_SDK_64% == Y ( + ECHO Configuring Windows SDK %WINDOWS_SDK_VERSION% for Python %MAJOR_PYTHON_VERSION% on a 64 bit architecture + SET DISTUTILS_USE_SDK=1 + SET MSSdk=1 + "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Setup\WindowsSdkVer.exe" -q -version:%WINDOWS_SDK_VERSION% + "%WIN_SDK_ROOT%\%WINDOWS_SDK_VERSION%\Bin\SetEnv.cmd" /x64 /release + ECHO Executing: %COMMAND_TO_RUN% + call %COMMAND_TO_RUN% || EXIT 1 + ) ELSE ( + ECHO Using default MSVC build environment for 64 bit architecture + ECHO Executing: %COMMAND_TO_RUN% + call %COMMAND_TO_RUN% || EXIT 1 + ) +) ELSE ( + ECHO Using default MSVC build environment for 32 bit architecture + ECHO Executing: %COMMAND_TO_RUN% + call %COMMAND_TO_RUN% || EXIT 1 +) From ab19580f467267add299615af12a576220471b34 Mon Sep 17 00:00:00 2001 From: Julian O Date: Thu, 10 Aug 2017 21:47:34 +1000 Subject: [PATCH 32/32] Solve Issue 629. --- moviepy/video/io/gif_writers.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/moviepy/video/io/gif_writers.py b/moviepy/video/io/gif_writers.py index b46e5c58f..b5f060710 100644 --- a/moviepy/video/io/gif_writers.py +++ b/moviepy/video/io/gif_writers.py @@ -278,8 +278,13 @@ def write_gif_with_image_io(clip, filename, fps=None, opt=0, loop=0, fps = clip.fps quantizer = 0 if opt!= 0 else 'nq' - writer = imageio.save(filename, duration=1.0/fps, - quantizer=quantizer, palettesize=colors) + writer = imageio.save( + filename, + duration=1.0/fps, + quantizer=quantizer, + palettesize=colors, + loop=loop + ) verbose_print(verbose, "\n[MoviePy] Building file %s with imageio\n"%filename)