From 4423b161e29ee93712fa5bcbf7e07fc5ea9faf58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Gait=C3=A1n?= Date: Thu, 8 Oct 2020 19:23:07 -0300 Subject: [PATCH] Fluent API for effects (monkeypatch as methods) (#1105) --- CHANGELOG.md | 3 +- moviepy/audio/AudioClip.py | 2 +- moviepy/audio/fx/__init__.py | 12 ++-- moviepy/audio/fx/all/__init__.py | 18 ++--- moviepy/audio/fx/audio_loop.py | 14 ++-- moviepy/editor.py | 85 +++++++++++++----------- moviepy/video/VideoClip.py | 2 +- moviepy/video/compositing/transitions.py | 3 + moviepy/video/fx/__init__.py | 36 ++++++++-- moviepy/video/fx/all/__init__.py | 19 ++---- moviepy/video/fx/time_mirror.py | 4 +- tests/test_issues.py | 1 + 12 files changed, 116 insertions(+), 83 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6fd9dc25..d4c5165e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -### Deprecated +### Deprecated +- `moviepy.video.fx.all` and `moviepy.audio.fx.all`. Use the fx method directly from the clip instance or import the fx function from `moviepy.video.fx` and `moviepy.audio.fx`. [#1105] ### Removed diff --git a/moviepy/audio/AudioClip.py b/moviepy/audio/AudioClip.py index 797e7c8b9..d167b35b0 100644 --- a/moviepy/audio/AudioClip.py +++ b/moviepy/audio/AudioClip.py @@ -44,7 +44,7 @@ class AudioClip(Clip): """ def __init__(self, make_frame=None, duration=None, fps=None): - Clip.__init__(self) + super().__init__() if fps is not None: self.fps = fps diff --git a/moviepy/audio/fx/__init__.py b/moviepy/audio/fx/__init__.py index 67fe3b194..8f2def3d5 100644 --- a/moviepy/audio/fx/__init__.py +++ b/moviepy/audio/fx/__init__.py @@ -1,4 +1,8 @@ -""" -This module contains transformation functions (clip->clip) -One file for one fx. The file's name is the fx's name -""" +# import every video fx function + +from .audio_fadein import audio_fadein +from .audio_fadeout import audio_fadeout +from .audio_left_right import audio_left_right +from .audio_loop import audio_loop +from .audio_normalize import audio_normalize +from .volumex import volumex diff --git a/moviepy/audio/fx/all/__init__.py b/moviepy/audio/fx/all/__init__.py index 02453c497..c36d6e87f 100644 --- a/moviepy/audio/fx/all/__init__.py +++ b/moviepy/audio/fx/all/__init__.py @@ -1,15 +1,11 @@ """ -Loads all the fx! -Usage: -import moviepy.audio.fx.all as afx -audio_clip = afx.volume_x(some_clip, .5) -""" - -import pkgutil +moviepy.audio.fx.all is deprecated. -import moviepy.audio.fx as fx +Use the fx method directly from the clip instance (e.g. ``clip.audio_loop(...)``) +or import the function from moviepy.audio.fx instead. +""" +import warnings -__all__ = [name for _, name, _ in pkgutil.iter_modules(fx.__path__) if name != "all"] +from .. import * -for name in __all__: - exec("from ..%s import %s" % (name, name)) +warnings.warn(f"\nMoviePy: {__doc__}", UserWarning) diff --git a/moviepy/audio/fx/audio_loop.py b/moviepy/audio/fx/audio_loop.py index 9781b3978..e8a3785ff 100644 --- a/moviepy/audio/fx/audio_loop.py +++ b/moviepy/audio/fx/audio_loop.py @@ -1,7 +1,9 @@ from ..AudioClip import concatenate_audioclips +from moviepy.decorators import audio_video_fx -def audio_loop(audioclip, n_loops=None, duration=None): +@audio_video_fx +def audio_loop(clip, n_loops=None, duration=None): """Loops over an audio clip. Returns an audio clip that plays the given clip either @@ -17,12 +19,8 @@ def audio_loop(audioclip, n_loops=None, duration=None): >>> videoclip.with_audio(audio) """ - if duration is not None: + n_loops = int(duration / clip.duration) + 1 + return concatenate_audioclips(n_loops * [clip]).with_duration(duration) - n_loops = int(duration / audioclip.duration) + 1 - return concatenate_audioclips(n_loops * [audioclip]).with_duration(duration) - - else: - - return concatenate_audioclips(n_loops * [audioclip]) + return concatenate_audioclips(n_loops * [clip]) diff --git a/moviepy/editor.py b/moviepy/editor.py index 2be06caf7..c7ec4a9e8 100644 --- a/moviepy/editor.py +++ b/moviepy/editor.py @@ -6,7 +6,7 @@ In particular it will load many effects from the video.fx and audio.fx folders and turn them into VideoClip methods, so that instead of ->>> clip.fx( vfx.resize, 2 ) # or equivalently vfx.resize(clip, 2) +>>> clip.fx( vfx.resize, 2 ) or equivalently vfx.resize(clip, 2) we can write >>> clip.resize(2) @@ -14,11 +14,36 @@ clip.preview(). """ +__all__ = [ + "afx", + "AudioClip", + "AudioFileClip", + "BitmapClip", + "clips_array", + "ColorClip", + "CompositeAudioClip", + "CompositeVideoClip", + "concatenate_audioclips", + "concatenate_videoclips", + "convert_to_seconds", + "download_webfile", + "ffmpeg_tools", + "ImageClip", + "ImageSequenceClip", + "ipython_display", + "TextClip", + "transfx", + "vfx", + "VideoClip", + "VideoFileClip", + "videotools", +] + # Note that these imports could have been performed in the __init__.py # file, but this would make the loading of moviepy slower. import os -import sys +import inspect # Hide the welcome message from pygame: https://github.com/pygame/pygame/issues/542 @@ -37,8 +62,8 @@ # FX -import moviepy.video.fx.all as vfx -import moviepy.audio.fx.all as afx +import moviepy.video.fx as vfx +import moviepy.audio.fx as afx import moviepy.video.compositing.transitions as transfx # Tools @@ -50,44 +75,26 @@ try: from .video.io.sliders import sliders + + __all__.append("sliders") except ImportError: pass -# The next loop transforms many effects into VideoClip methods so that -# they can be called with myclip.resize(width=500) instead of -# myclip.fx( vfx.resize, width= 500) -for method in [ - "afx.audio_fadein", - "afx.audio_fadeout", - "afx.audio_normalize", - "afx.volumex", - "transfx.crossfadein", - "transfx.crossfadeout", - "vfx.crop", - "vfx.fadein", - "vfx.fadeout", - "vfx.invert_colors", - "vfx.loop", - "vfx.margin", - "vfx.mask_and", - "vfx.mask_or", - "vfx.resize", - "vfx.rotate", - "vfx.speedx", -]: - - exec("VideoClip.%s = %s" % (method.split(".")[1], method)) - - -for method in [ - "afx.audio_fadein", - "afx.audio_fadeout", - "afx.audio_loop", - "afx.audio_normalize", - "afx.volumex", -]: - - exec("AudioClip.%s = %s" % (method.split(".")[1], method)) +# Transforms the effects into Clip methods so that +# they can be called with clip.resize(width=500) instead of +# clip.fx(vfx.resize, width=500) +audio_fxs = inspect.getmembers(afx, inspect.isfunction) +video_fxs = ( + inspect.getmembers(vfx, inspect.isfunction) + + inspect.getmembers(transfx, inspect.isfunction) + + audio_fxs +) + +for name, function in video_fxs: + setattr(VideoClip, name, function) + +for name, function in audio_fxs: + setattr(AudioClip, name, function) # adds easy ipython integration diff --git a/moviepy/video/VideoClip.py b/moviepy/video/VideoClip.py index 9a6174a5d..a034088fc 100644 --- a/moviepy/video/VideoClip.py +++ b/moviepy/video/VideoClip.py @@ -90,7 +90,7 @@ class VideoClip(Clip): def __init__( self, make_frame=None, is_mask=False, duration=None, has_constant_size=True ): - Clip.__init__(self) + super().__init__() self.mask = None self.audio = None self.pos = lambda t: (0, 0) diff --git a/moviepy/video/compositing/transitions.py b/moviepy/video/compositing/transitions.py index 05a8d00c9..4d1ff6d38 100644 --- a/moviepy/video/compositing/transitions.py +++ b/moviepy/video/compositing/transitions.py @@ -11,6 +11,9 @@ from .CompositeVideoClip import CompositeVideoClip +__all__ = ["crossfadein", "crossfadeout", "slide_in", "slide_out", "make_loopable"] + + @requires_duration @add_mask_if_none def crossfadein(clip, duration): diff --git a/moviepy/video/fx/__init__.py b/moviepy/video/fx/__init__.py index 67fe3b194..4dc78e689 100644 --- a/moviepy/video/fx/__init__.py +++ b/moviepy/video/fx/__init__.py @@ -1,4 +1,32 @@ -""" -This module contains transformation functions (clip->clip) -One file for one fx. The file's name is the fx's name -""" +# import every video fx function + +from .accel_decel import accel_decel +from .blackwhite import blackwhite +from .blink import blink +from .colorx import colorx +from .crop import crop +from .even_size import even_size +from .fadein import fadein +from .fadeout import fadeout +from .freeze import freeze +from .freeze_region import freeze_region +from .gamma_corr import gamma_corr +from .headblur import headblur +from .invert_colors import invert_colors +from .loop import loop +from .lum_contrast import lum_contrast +from .make_loopable import make_loopable +from .margin import margin +from .mask_and import mask_and +from .mask_color import mask_color +from .mask_or import mask_or +from .mirror_x import mirror_x +from .mirror_y import mirror_y +from .painting import painting +from .resize import resize +from .rotate import rotate +from .scroll import scroll +from .speedx import speedx +from .supersample import supersample +from .time_mirror import time_mirror +from .time_symmetrize import time_symmetrize diff --git a/moviepy/video/fx/all/__init__.py b/moviepy/video/fx/all/__init__.py index 8aebe51ab..47364b51e 100644 --- a/moviepy/video/fx/all/__init__.py +++ b/moviepy/video/fx/all/__init__.py @@ -1,16 +1,11 @@ """ -Loads all the fx ! -Usage: -import moviepy.video.fx.all as vfx -clip = vfx.resize(some_clip, width=400) -clip = vfx.mirror_x(some_clip) -""" - -import pkgutil +moviepy.video.fx.all is deprecated. -import moviepy.video.fx as fx +Use the fx method directly from the clip instance (e.g. ``clip.resize(...)``) +or import the function from moviepy.video.fx instead. +""" +import warnings -__all__ = [name for _, name, _ in pkgutil.iter_modules(fx.__path__) if name != "all"] +from .. import * -for name in __all__: - exec("from ..%s import %s" % (name, name)) +warnings.warn(f"\nMoviePy: {__doc__}", UserWarning) diff --git a/moviepy/video/fx/time_mirror.py b/moviepy/video/fx/time_mirror.py index ef5f071ce..f5cc38cae 100644 --- a/moviepy/video/fx/time_mirror.py +++ b/moviepy/video/fx/time_mirror.py @@ -4,10 +4,10 @@ @requires_duration @apply_to_mask @apply_to_audio -def time_mirror(self): +def time_mirror(clip): """ Returns a clip that plays the current clip backwards. The clip must have its ``duration`` attribute set. The same effect is applied to the clip's audio and mask if any. """ - return self.time_transform(lambda t: self.duration - t - 1, keep_duration=True) + return clip.time_transform(lambda t: clip.duration - t - 1, keep_duration=True) diff --git a/tests/test_issues.py b/tests/test_issues.py index 58fa14b08..9bc086d9d 100644 --- a/tests/test_issues.py +++ b/tests/test_issues.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """Issue tests meant to be run with pytest.""" import pytest +import os from moviepy.editor import * from moviepy.utils import close_all_clips