Skip to content
2 changes: 2 additions & 0 deletions pyclip/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

from .core.video import Video
21 changes: 20 additions & 1 deletion pyclip/core/video.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
from __future__ import annotations

from pathlib import Path

class Model:
pass

class Video(Model):

def trim(self):
def trim(self, start: float, end: float | None = None, unit: str = "ms") -> Video:
"""
Returns a clip playing the content of the current clip
between times `start` and `end`
"""
...

def mute(self, channels: int | list[int] | None = None):
Expand All @@ -11,4 +21,13 @@ def resize(self, **kwargs):
...

def upscale(self, **kwargs):
...

def rand(self):
...

def open(self, path: str | Path):
...

def save(self, path: str | Path):
...
17 changes: 17 additions & 0 deletions tests/test_conversion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Conversion operation tests

References:
- https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.save

"""

from pyclip import Video
import tempfile

import pytest

@pytest.mark.parametrize("file_format", [("mp4"), ("avi")])
def test_video_conversion(mock_video: Video, file_format: str):
with tempfile.NamedTemporaryFile(suffix=f".{file_format}") as f:
mock_video.save(f.name, format=file_format.upper())
assert Video.open(f.name) == mock_video
84 changes: 84 additions & 0 deletions tests/test_flip.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Tests for the rotate and flip methods of the Video class.

References:
- https://kornia.readthedocs.io/en/latest/geometry.transform.html#kornia.geometry.transform.hflip
- https://kornia.readthedocs.io/en/latest/geometry.transform.html#kornia.geometry.transform.vflip

"""

import pyclip
import pytest

def test_flip_horizontal(mock_video: pyclip.Video):
"""Test flipping a video horizontally."""
flipped_once = mock_video.fliph()
assert flipped_once != mock_video

flipped_twice = flipped_once.fliph()
assert flipped_twice == mock_video

def test_flip_vertical(mock_video: pyclip.Video):
"""Test flipping a video vertically."""
flipped_once = mock_video.flipv()
assert flipped_once != mock_video

flipped_twice = flipped_once.flipv()
assert flipped_twice == mock_video

def test_flip_both(mock_video: pyclip.Video):
"""Test flipping a video both horizontally and vertically."""
assert mock_video.flip(horizontal=True, vertical=True) == mock_video.flipv().fliph() == mock_video.fliph().flipv()

def test_flip_without_args(mock_video: pyclip.Video):
"""Test flipping without any arguments should raise a ValueError."""
with pytest.raises(ValueError):
mock_video.flip()

def test_flip_audio_sync(mock_video: pyclip.Video):
"""Test audio synchronization after flipping."""
flipped = mock_video.fliph()
assert flipped.audio == mock_video.audio

def test_flip_horizontal_and_vertical(mock_video: pyclip.Video):
"""Test flipping a video horizontally and then vertically should equal flipping both at once."""
flipped_sequentially = mock_video.fliph().flipv()
flipped_simultaneously = mock_video.flip(horizontal=True, vertical=True)
assert flipped_sequentially == flipped_simultaneously

def test_flip_vertical_audio_sync(mock_video: pyclip.Video):
"""Test audio synchronization after vertical flipping."""
flipped = mock_video.flipv()
assert flipped.audio == mock_video.audio

def test_flip_invalid_arguments(mock_video: pyclip.Video):
"""Test flipping with an invalid argument type should raise a TypeError."""
with pytest.raises(TypeError):
mock_video.flip(horizontal="True", vertical=True)

def test_fliph_method_vs_flip_function(mock_video: pyclip.Video):
"""Test equivalence of fliph method vs flip function with horizontal=True."""
assert mock_video.fliph() == mock_video.flip(horizontal=True)

def test_flipv_method_vs_flip_function(mock_video: pyclip.Video):
"""Test equivalence of flipv method vs flip function with vertical=True."""
assert mock_video.flipv() == mock_video.flip(vertical=True)

def test_flip_with_both_false(mock_video: pyclip.Video):
"""Test flipping with both horizontal and vertical set to False should return the original."""
assert mock_video.flip(horizontal=False, vertical=False) == mock_video

def test_flip_idempotence(mock_video: pyclip.Video):
"""Test that flipping twice in any direction should return the original video."""
assert mock_video.flip(horizontal=True, vertical=True).flip(horizontal=True, vertical=True) == mock_video.fliph().fliph() == mock_video.flipv().flipv() == mock_video

def test_flip_chain_operations(mock_video: pyclip.Video):
"""Test chaining multiple flip operations."""
result = mock_video.fliph().flipv().fliph()
assert result == mock_video.flipv()

def test_flip_no_audio(mock_video: pyclip.Video):
"""Test flipping a video with no audio."""
# Assuming the Video class has a method to remove audio
video_no_audio = mock_video.remove_audio()
flipped = video_no_audio.fliph()
assert flipped.audio is None
72 changes: 72 additions & 0 deletions tests/test_rotate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""Tests for the rotate and flip methods of the Video class.

References:
- https://kornia.readthedocs.io/en/latest/geometry.transform.html#kornia.geometry.transform.rotate

"""

import pyclip
import pytest

def test_rotate_90_degrees(mock_video: pyclip.Video):
"""Test rotating a video 90 degrees counter-clockwise."""
rotated = mock_video.rotate(90)
assert rotated.width == mock_video.width
assert rotated.height == mock_video.height

def test_rotate_with_center(mock_video: pyclip.Video):
"""Test rotating a video about a specific center."""
center = [mock_video.width // 2, mock_video.height // 2]
assert mock_video.rotate(45, center=center) == mock_video.rotate(45)

def test_rotate_180_degrees(mock_video: pyclip.Video):
"""Test rotating a video 180 degrees counter-clockwise and checking its properties."""
assert mock_video.rotate(180) == mock_video.flipv()

def test_rotate_360_degrees(mock_video: pyclip.Video):
"""Test rotating a video 360 degrees should be equal to the original."""
assert mock_video.rotate(360) == mock_video

def test_rotate_with_interpolation(mock_video: pyclip.Video):
"""Test rotating a video with a specific interpolation."""
rotated_bilinear = mock_video.rotate(45, mode='bilinear')
rotated_nearest = mock_video.rotate(45, mode='nearest')

assert rotated_bilinear != rotated_nearest
assert rotated_bilinear.is_close(rotated_nearest)

def test_rotate_with_padding_mode(mock_video: pyclip.Video):
"""Test rotating a video with different padding modes."""
rotated_zeros = mock_video.rotate(45, padding_mode='zeros')
rotated_border = mock_video.rotate(45, padding_mode='border')
rotated_reflection = mock_video.rotate(45, padding_mode='reflection')

# Assuming Video class has a method to compare video content, e.g., is_close()
assert not rotated_zeros.is_close(rotated_border)
assert not rotated_zeros.is_close(rotated_reflection)
assert not rotated_border.is_close(rotated_reflection)

def test_rotate_audio_sync(mock_video: pyclip.Video):
"""Test audio synchronization after rotation."""
rotated = mock_video.rotate(45)
assert rotated.audio == mock_video.audio

def test_rotate_invalid_angle(mock_video: pyclip.Video):
"""Test rotating with an invalid angle type should raise a TypeError."""
with pytest.raises(TypeError):
mock_video.rotate("invalid_angle")

def test_rotate_invalid_interpolation(mock_video: pyclip.Video):
"""Test rotating with an invalid interpolation mode should raise a ValueError."""
with pytest.raises(ValueError):
mock_video.rotate(45, mode='invalid_mode')

def test_rotate_invalid_padding_mode(mock_video: pyclip.Video):
"""Test rotating with an invalid padding mode should raise a ValueError."""
with pytest.raises(ValueError):
mock_video.rotate(45, padding_mode='invalid_padding')

def test_rotate_invalid_center(mock_video: pyclip.Video):
"""Test rotating with an invalid center type should raise a TypeError."""
with pytest.raises(TypeError):
mock_video.rotate(45, center="invalid_center")
53 changes: 45 additions & 8 deletions tests/test_trim.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,59 @@
"""
Tests for the trim method of the Video class.

References:
- https://zulko.github.io/moviepy/ref/Clip.html?highlight=subclip#moviepy.Clip.Clip.subclip

"""

import pyclip
from pyclip import Trim

import pytest

def test_sanity():
assert 1 == 1

@pytest.fixture
def mock_video():
return pyclip.RandomVideo()
def mock_video() -> pyclip.Video:
"""Fixture to generate a mock video for testing."""
return pyclip.Video.rand()

def test_trim(mock_video):
def test_trim(mock_video: pyclip.Video):
"""Test trimming a video using seconds as unit."""
video = mock_video.trim(50, 1000)
assert video.duration == 950

def test_trim_by_frames(mock_video):
def test_trim_by_frames(mock_video: pyclip.Video):
"""Test trimming a video using frames as unit."""
assert mock_video.trim(50, 100, unit="f") == mock_video[50:100]

def test_trim_negative(mock_video):
def test_trim_negative(mock_video: pyclip.Video):
"""Test trimming with negative start frame raises a ValueError."""
with pytest.raises(ValueError):
mock_video.trim(-100, 1000, unit="f")

def test_trim_class_operation(mock_video: pyclip.Video):
"""Test using Trim class operation to trim a video."""
transforms = [
Trim(50, 100, unit="f")
]

assert mock_video.apply(transforms) == mock_video.trim(50, 100, unit="f")

def test_trim_start_greater_than_end(mock_video: pyclip.Video):
"""Test trimming with start time greater than end time raises a ValueError."""
with pytest.raises(ValueError):
mock_video.trim(1000, 100)

def test_trim_exceed_duration(mock_video: pyclip.Video):
"""Test trimming with time range exceeding video duration raises an IndexError."""
with pytest.raises(IndexError):
mock_video.trim(0, mock_video.duration + 1000)

def test_trim_invalid_unit(mock_video: pyclip.Video):
"""Test trimming with invalid unit raises a ValueError."""
with pytest.raises(ValueError):
mock_video.trim(0, 100, unit="z")

def test_trim_without_end(mock_video: pyclip.Video):
"""Test trimming without specifying end trims to the end of the video."""
trimmed = mock_video.trim(50)
assert trimmed.duration == mock_video.duration - 50