diff --git a/scenedetect/output/__init__.py b/scenedetect/output/__init__.py index 0766d2a8..c96ee877 100644 --- a/scenedetect/output/__init__.py +++ b/scenedetect/output/__init__.py @@ -152,6 +152,7 @@ def write_scene_list( Raises: TypeError: "delimiter" must be a 1-character string """ + # `_open_output_file` replaces filesystem paths with writable text streams. assert not isinstance(output_csv_file, (str, bytes, os.PathLike)) output_file = output_csv_file csv_writer = csv.writer(output_file, delimiter=col_separator, lineterminator=row_separator) @@ -374,6 +375,7 @@ def write_scene_list_edl( every event so the EDL aligns with the source media's on-screen timecode. Applied to both source and record columns. """ + # `_open_output_file` replaces filesystem paths with writable text streams. assert not isinstance(output_path, (str, bytes, os.PathLike)) output_file = output_path offset_frames = 0 @@ -436,6 +438,7 @@ def write_scene_list_fcpx( of `video_path`. """ assert scene_list + # `_open_output_file` replaces filesystem paths with writable text streams. assert not isinstance(output_path, (str, bytes, os.PathLike)) output_file = output_path video_path = Path(video_path) @@ -540,6 +543,7 @@ def write_scene_list_fcp7( frozen. If None, falls back to the last scene's end time. """ assert scene_list + # `_open_output_file` replaces filesystem paths with writable text streams. assert not isinstance(output_path, (str, bytes, os.PathLike)) output_file = output_path video_path = Path(video_path) @@ -654,6 +658,7 @@ def write_scene_list_otio( name: Timeline name. Defaults to the stem of `video_path`. audio: If True (default), include an audio track alongside the video track. """ + # `_open_output_file` replaces filesystem paths with writable text streams. assert not isinstance(output_path, (str, bytes, os.PathLike)) output_file = output_path video_path = Path(video_path) diff --git a/tests/test_output.py b/tests/test_output.py index b419cbfd..6215f835 100644 --- a/tests/test_output.py +++ b/tests/test_output.py @@ -32,6 +32,7 @@ from scenedetect.output import ( SceneMetadata, VideoMetadata, + _open_output_file, is_ffmpeg_available, split_video_ffmpeg, write_scene_list, @@ -273,6 +274,20 @@ def _make_output_target( return output_target, lambda: output_path.read_text(encoding="utf-8") +def test_open_output_file_does_not_create_file_on_error(tmp_path: Path): + """The decorator does not create a destination when output generation fails.""" + + @_open_output_file("TEST") + def failing_writer(output_file: ty.Any): + output_file.write("partial output") + raise RuntimeError("generation failed") + + output_path = tmp_path / "output.txt" + with pytest.raises(RuntimeError, match="generation failed"): + failing_writer(output_path) + assert not output_path.exists() + + @pytest.mark.parametrize("target_kind", _OUTPUT_TARGET_KINDS) def test_write_scene_list_output_target(tmp_path: Path, target_kind: str): """CSV output accepts a file handle, string, Path, or bytes path."""