Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type hints #1962

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,6 @@ tests/media

# Documentation
docs/build/

# Virtual environment
.venv/
72 changes: 50 additions & 22 deletions moviepy/Clip.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@
are common to the two subclasses of Clip, VideoClip and AudioClip.
"""

from __future__ import annotations

import copy as _copy
from collections.abc import Callable, Iterator
from typing import TYPE_CHECKING

import numpy as np
import proglog
from typing_extensions import Concatenate, Literal, Self, TypeAlias

from moviepy.decorators import (
apply_to_audio,
Expand All @@ -16,6 +21,11 @@
use_clip_fps_by_default,
)

if TYPE_CHECKING:
from moviepy.types import Logger, P, T, Time

_ApplyTo: TypeAlias = Literal["audio", "movie"]


class Clip:
"""Base class of all clips (VideoClips and AudioClips).
Expand Down Expand Up @@ -43,9 +53,9 @@ class Clip:
_TEMP_FILES_PREFIX = "TEMP_MPY_"

def __init__(self):
self.start = 0
self.end = None
self.duration = None
self.start: float = 0
self.end: float | None = None
self.duration: float | None = None

self.memoize = False
self.memoized_t = None
Expand All @@ -56,7 +66,7 @@ def copy(self):
return _copy.copy(self)

@convert_parameter_to_seconds(["t"])
def get_frame(self, t):
def get_frame(self, t: Time) -> np.ndarray[..., ...]:
"""Gets a numpy array representing the RGB picture of the clip,
or (mono or stereo) value for a sound clip, at time ``t``.

Expand All @@ -79,7 +89,12 @@ def get_frame(self, t):
# print(t)
return self.make_frame(t)

def transform(self, func, apply_to=None, keep_duration=True):
def transform(
self,
func: Callable[[Callable[[Time], ...], float], np.ndarray[..., ...]],
apply_to: _ApplyTo | list[_ApplyTo] | None = None,
keep_duration: bool = True,
) -> Self:
"""General processing of a clip.

Returns a new Clip whose frames are a transformation
Expand Down Expand Up @@ -139,7 +154,12 @@ def transform(self, func, apply_to=None, keep_duration=True):

return new_clip

def time_transform(self, time_func, apply_to=None, keep_duration=False):
def time_transform(
self,
time_func: Callable[[Time], Time],
apply_to: _ApplyTo | list[_ApplyTo] | None = None,
keep_duration: bool = False,
) -> Self:
"""
Returns a Clip instance playing the content of the current clip
but with a modified timeline, time ``t`` being replaced by another
Expand Down Expand Up @@ -179,7 +199,9 @@ def time_transform(self, time_func, apply_to=None, keep_duration=False):
keep_duration=keep_duration,
)

def fx(self, func, *args, **kwargs):
def fx(
self, func: Callable[Concatenate[Self, P], T], *args: P.args, **kwargs: P.kwargs
) -> T:
"""Returns the result of ``func(self, *args, **kwargs)``, for instance

>>> new_clip = clip.fx(resize, 0.2, method="bilinear")
Expand All @@ -202,7 +224,7 @@ def fx(self, func, *args, **kwargs):
@apply_to_audio
@convert_parameter_to_seconds(["t"])
@outplace
def with_start(self, t, change_end=True):
def with_start(self, t: Time, change_end: bool = True) -> None:
"""Returns a copy of the clip, with the ``start`` attribute set
to ``t``, which can be expressed in seconds (15.35), in (min, sec),
in (hour, min, sec), or as a string: '01:03:05.35'.
Expand Down Expand Up @@ -234,7 +256,7 @@ def with_start(self, t, change_end=True):
@apply_to_audio
@convert_parameter_to_seconds(["t"])
@outplace
def with_end(self, t):
def with_end(self, t: Time) -> None:
"""Returns a copy of the clip, with the ``end`` attribute set to ``t``,
which can be expressed in seconds (15.35), in (min, sec), in
(hour, min, sec), or as a string: '01:03:05.35'. Also sets the duration
Expand All @@ -259,7 +281,7 @@ def with_end(self, t):
@apply_to_audio
@convert_parameter_to_seconds(["duration"])
@outplace
def with_duration(self, duration, change_end=True):
def with_duration(self, duration: float, change_end: bool = True) -> None:
"""Returns a copy of the clip, with the ``duration`` attribute set to
``t``, which can be expressed in seconds (15.35), in (min, sec), in
(hour, min, sec), or as a string: '01:03:05.35'. Also sets the duration
Expand Down Expand Up @@ -288,7 +310,7 @@ def with_duration(self, duration, change_end=True):
self.start = self.end - duration

@outplace
def with_make_frame(self, make_frame):
def with_make_frame(self, make_frame: Callable):
"""Sets a ``make_frame`` attribute for the clip. Useful for setting
arbitrary/complicated videoclips.

Expand All @@ -300,7 +322,7 @@ def with_make_frame(self, make_frame):
"""
self.make_frame = make_frame

def with_fps(self, fps, change_duration=False):
def with_fps(self, fps: int, change_duration: bool = False) -> Self:
"""Returns a copy of the clip with a new default fps for functions like
write_videofile, iterframe, etc.

Expand All @@ -326,7 +348,7 @@ def with_fps(self, fps, change_duration=False):
return newclip

@outplace
def with_is_mask(self, is_mask):
def with_is_mask(self, is_mask: bool) -> None:
"""Says whether the clip is a mask or not.

Parameters
Expand All @@ -338,7 +360,7 @@ def with_is_mask(self, is_mask):
self.is_mask = is_mask

@outplace
def with_memoize(self, memoize):
def with_memoize(self, memoize: bool) -> None:
"""Sets whether the clip should keep the last frame read in memory.

Parameters
Expand All @@ -350,7 +372,7 @@ def with_memoize(self, memoize):
self.memoize = memoize

@convert_parameter_to_seconds(["t"])
def is_playing(self, t):
def is_playing(self, t: Time | np.ndarray[..., ...]) -> ...:
"""If ``t`` is a time, returns true if t is between the start and the end
of the clip. ``t`` can be expressed in seconds (15.35), in (min, sec), in
(hour, min, sec), or as a string: '01:03:05.35'. If ``t`` is a numpy
Expand Down Expand Up @@ -379,7 +401,7 @@ def is_playing(self, t):
@convert_parameter_to_seconds(["start_time", "end_time"])
@apply_to_mask
@apply_to_audio
def subclip(self, start_time=0, end_time=None):
def subclip(self, start_time: Time = 0, end_time: Time | None = None) -> Self:
"""Returns a clip playing the content of the current clip between times
``start_time`` and ``end_time``, which can be expressed in seconds
(15.35), in (min, sec), in (hour, min, sec), or as a string:
Expand Down Expand Up @@ -444,7 +466,7 @@ def subclip(self, start_time=0, end_time=None):
return new_clip

@convert_parameter_to_seconds(["start_time", "end_time"])
def cutout(self, start_time, end_time):
def cutout(self, start_time: Time, end_time: Time) -> Self:
"""
Returns a clip playing the content of the current clip but
skips the extract between ``start_time`` and ``end_time``, which can be
Expand Down Expand Up @@ -479,7 +501,13 @@ def cutout(self, start_time, end_time):

@requires_duration
@use_clip_fps_by_default
def iter_frames(self, fps=None, with_times=False, logger=None, dtype=None):
def iter_frames(
self,
fps: int | None = None,
with_times: bool = False,
logger: Logger = None,
dtype: np.dtype | None = None,
) -> Iterator[...]:
"""Iterates over all the frames of the clip.

Returns each frame of the clip as a HxWxN Numpy array,
Expand Down Expand Up @@ -533,7 +561,7 @@ def iter_frames(self, fps=None, with_times=False, logger=None, dtype=None):
else:
yield frame

def close(self):
def close(self) -> None:
"""Release any resources that are in use."""
# Implementation note for subclasses:
#
Expand All @@ -545,7 +573,7 @@ def close(self):
# * Therefore, should NOT be called by __del__().
pass

def __eq__(self, other):
def __eq__(self, other: object) -> bool:
if not isinstance(other, Clip):
return NotImplemented

Expand All @@ -564,8 +592,8 @@ def __eq__(self, other):

# Support the Context Manager protocol, to ensure that resources are cleaned up.

def __enter__(self):
def __enter__(self) -> Self:
return self

def __exit__(self, exc_type, exc_value, traceback):
def __exit__(self, *_) -> None:
self.close()
45 changes: 33 additions & 12 deletions moviepy/audio/AudioClip.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,26 @@
- Composition: CompositeAudioClip
"""

from __future__ import annotations

import numbers
import os
from typing import TYPE_CHECKING

import numpy as np
import proglog
from typing_extensions import Literal

from moviepy.audio.io.ffmpeg_audiowriter import ffmpeg_audiowrite
from moviepy.Clip import Clip
from moviepy.decorators import convert_path_to_string, requires_duration
from moviepy.tools import extensions_dict

if TYPE_CHECKING:
from typing import Any, Callable

from moviepy.types import Logger


class AudioClip(Clip):
"""Base class for audio clips.
Expand Down Expand Up @@ -62,7 +71,12 @@ class AudioClip(Clip):

"""

def __init__(self, make_frame=None, duration=None, fps=None):
def __init__(
self,
make_frame: Callable[[...], ...] | None = None,
duration: float | None = None,
fps: int | None = None,
) -> None:
super().__init__()

if fps is not None:
Expand All @@ -84,10 +98,10 @@ def iter_chunks(
self,
chunksize=None,
chunk_duration=None,
fps=None,
quantize=False,
nbytes=2,
logger=None,
fps: int | None = None,
quantize: bool = False,
nbytes: Literal[1, 2, 4] = 2,
logger: Logger = None,
):
"""Iterator that returns the whole sound array of the clip by chunks"""
if fps is None:
Expand All @@ -112,7 +126,12 @@ def iter_chunks(

@requires_duration
def to_soundarray(
self, tt=None, fps=None, quantize=False, nbytes=2, buffersize=50000
self,
tt=None,
fps: int | None = None,
quantize: bool = False,
nbytes: Literal[1, 2, 4] = 2,
buffersize: int = 50000,
):
"""
Transforms the sound into an array that can be played by pygame
Expand Down Expand Up @@ -163,7 +182,9 @@ def to_soundarray(

return snd_array

def max_volume(self, stereo=False, chunksize=50000, logger=None):
def max_volume(
self, stereo: bool = False, chunksize: int = 50000, logger: Logger = None
):
"""Returns the maximum volume level of the clip."""
# max volume separated by channels if ``stereo`` and not mono
stereo = stereo and self.nchannels > 1
Expand All @@ -180,15 +201,15 @@ def max_volume(self, stereo=False, chunksize=50000, logger=None):
@convert_path_to_string("filename")
def write_audiofile(
self,
filename,
fps=None,
filename: str | os.PathLike[Any],
fps: int | None = None,
nbytes=2,
buffersize=2000,
codec=None,
bitrate=None,
ffmpeg_params=None,
write_logfile=False,
logger="bar",
logger: Logger = "bar",
):
"""Writes an audio file from the AudioClip.

Expand Down Expand Up @@ -278,7 +299,7 @@ class AudioArrayClip(AudioClip):

"""

def __init__(self, array, fps):
def __init__(self, array, fps: int) -> None:
Clip.__init__(self)
self.array = array
self.fps = fps
Expand Down Expand Up @@ -367,7 +388,7 @@ def make_frame(self, t):
return zero + sum(sounds)


def concatenate_audioclips(clips):
def concatenate_audioclips(clips: list[AudioClip]) -> CompositeAudioClip:
"""Concatenates one AudioClip after another, in the order that are passed
to ``clips`` parameter.

Expand Down
11 changes: 10 additions & 1 deletion moviepy/audio/fx/audio_delay.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
from __future__ import annotations

from typing import TYPE_CHECKING

import numpy as np

from moviepy.audio.AudioClip import CompositeAudioClip
from moviepy.audio.fx.multiply_volume import multiply_volume
from moviepy.decorators import audio_video_fx

if TYPE_CHECKING:
from moviepy.Clip import Clip


@audio_video_fx
def audio_delay(clip, offset=0.2, n_repeats=8, decay=1):
def audio_delay(
clip: Clip, offset: float = 0.2, n_repeats: int = 8, decay: float = 1
) -> CompositeAudioClip:
"""Repeats audio certain number of times at constant intervals multiplying
their volume levels using a linear space in the range 1 to ``decay`` argument
value.
Expand Down
10 changes: 9 additions & 1 deletion moviepy/audio/fx/audio_fadein.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
from __future__ import annotations

from typing import TYPE_CHECKING

import numpy as np

from moviepy.decorators import audio_video_fx, convert_parameter_to_seconds

if TYPE_CHECKING:
from moviepy.Clip import Clip
from moviepy.audio.AudioClip import AudioClip


def _mono_factor_getter():
return lambda t, duration: np.minimum(t / duration, 1)
Expand All @@ -17,7 +25,7 @@ def getter(t, duration):

@audio_video_fx
@convert_parameter_to_seconds(["duration"])
def audio_fadein(clip, duration):
def audio_fadein(clip: AudioClip, duration: float) -> Clip:
"""Return an audio (or video) clip that is first mute, then the
sound arrives progressively over ``duration`` seconds.

Expand Down
Loading