diff --git a/pyproject.toml b/pyproject.toml index 182167ee..8186012b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,26 +27,26 @@ docstring-code-format = true [tool.ruff.lint] select = [ + # flake8-bugbear + "B", # pycodestyle "E", # Pyflakes "F", - # TODO - Rule sets to enable: + # isort + "I", + # TODO - Add additional rule sets (https://docs.astral.sh/ruff/rules/): # pyupgrade #"UP", - # flake8-bugbear - #"B", # flake8-simplify #"SIM", - # isort - #"I", ] ignore = [ # TODO: Determine if we should use __all__, a reudndant alias, or keep this suppressed. "F401", - # Line too long + # TODO: Line too long "E501", - # Do not assign a `lambda` expression, use a `def` + # TODO: Do not assign a `lambda` expression, use a `def` "E731", ] fixable = ["ALL"] diff --git a/scenedetect/__init__.py b/scenedetect/__init__.py index e3db7906..544be977 100644 --- a/scenedetect/__init__.py +++ b/scenedetect/__init__.py @@ -14,7 +14,6 @@ can be used to open a video for a :class:`SceneManager `. """ -# ruff: noqa: I001 from logging import getLogger from typing import List, Optional, Tuple, Union @@ -31,7 +30,7 @@ ) from ex # Commonly used classes/functions exported under the `scenedetect` namespace for brevity. -from scenedetect.platform import init_logger +from scenedetect.platform import init_logger # noqa: I001 from scenedetect.frame_timecode import FrameTimecode from scenedetect.video_stream import VideoStream, VideoOpenFailure from scenedetect.video_splitter import split_video_ffmpeg, split_video_mkvmerge @@ -52,9 +51,7 @@ ) from scenedetect.stats_manager import StatsManager, StatsFileCorrupt from scenedetect.scene_manager import SceneManager, save_images - -# [DEPRECATED] DO NOT USE. -from scenedetect.video_manager import VideoManager +from scenedetect.video_manager import VideoManager # [DEPRECATED] DO NOT USE. # Used for module identification and when printing version & about info # (e.g. calling `scenedetect version` or `scenedetect about`). diff --git a/scenedetect/__main__.py b/scenedetect/__main__.py index 0328a343..7c9ec1b9 100755 --- a/scenedetect/__main__.py +++ b/scenedetect/__main__.py @@ -53,7 +53,7 @@ def main(): raise else: logger.critical("Unhandled exception:", exc_info=ex) - raise SystemExit(1) + raise SystemExit(1) from None if __name__ == "__main__": diff --git a/scenedetect/_cli/__init__.py b/scenedetect/_cli/__init__.py index 2af76bc4..18047181 100644 --- a/scenedetect/_cli/__init__.py +++ b/scenedetect/_cli/__init__.py @@ -17,7 +17,6 @@ """ # Some parts of this file need word wrap to be displayed. -# pylint: disable=line-too-long import inspect import logging @@ -263,7 +262,6 @@ def _print_command_help(ctx: click.Context, command: click.Command): help="Suppress output to terminal/stdout. Equivalent to setting --verbosity=none.", ) @click.pass_context -# pylint: disable=redefined-builtin def scenedetect( ctx: click.Context, input: ty.Optional[ty.AnyStr], @@ -327,9 +325,6 @@ def scenedetect( ) -# pylint: enable=redefined-builtin - - @click.command("help", cls=_Command) @click.argument( "command_name", diff --git a/scenedetect/_cli/config.py b/scenedetect/_cli/config.py index 037cf1b1..929587ad 100644 --- a/scenedetect/_cli/config.py +++ b/scenedetect/_cli/config.py @@ -234,7 +234,7 @@ def format(self, timecode: FrameTimecode) -> str: return timecode.get_timecode() if self == TimecodeFormat.SECONDS: return "%.3f" % timecode.get_seconds() - assert False + raise RuntimeError("Unhandled format specifier.") ConfigValue = Union[bool, int, float, str] @@ -558,9 +558,9 @@ def _load_from_disk(self, path=None): config_file_contents = config_file.read() config.read_string(config_file_contents, source=path) except ParsingError as ex: - raise ConfigLoadFailure(self._init_log, reason=ex) + raise ConfigLoadFailure(self._init_log, reason=ex) from None except OSError as ex: - raise ConfigLoadFailure(self._init_log, reason=ex) + raise ConfigLoadFailure(self._init_log, reason=ex) from None # At this point the config file syntax is correct, but we need to still validate # the parsed options (i.e. that the options have valid values). errors = _validate_structure(config) diff --git a/scenedetect/_cli/context.py b/scenedetect/_cli/context.py index ad88890d..de0e95a0 100644 --- a/scenedetect/_cli/context.py +++ b/scenedetect/_cli/context.py @@ -102,7 +102,6 @@ def check_split_video_requirements(use_mkvmerge: bool) -> None: raise click.BadParameter(error_str, param_hint="split-video") -# pylint: disable=too-many-instance-attributes,too-many-arguments,too-many-locals class CliContext: """Context of the command-line interface and config file parameters passed between sub-commands. @@ -324,7 +323,7 @@ def handle_options( scene_manager.downscale = downscale except ValueError as ex: logger.debug(str(ex)) - raise click.BadParameter(str(ex), param_hint="downscale factor") + raise click.BadParameter(str(ex), param_hint="downscale factor") from None scene_manager.interpolation = Interpolation[ self.config.get_value("global", "downscale-method").upper() ] @@ -357,7 +356,7 @@ def get_detect_content_params( weights = ContentDetector.Components(*weights) except ValueError as ex: logger.debug(str(ex)) - raise click.BadParameter(str(ex), param_hint="weights") + raise click.BadParameter(str(ex), param_hint="weights") from None return { "weights": self.config.get_value("detect-content", "weights", weights), @@ -415,7 +414,7 @@ def get_detect_adaptive_params( weights = ContentDetector.Components(*weights) except ValueError as ex: logger.debug(str(ex)) - raise click.BadParameter(str(ex), param_hint="weights") + raise click.BadParameter(str(ex), param_hint="weights") from None return { "adaptive_threshold": self.config.get_value("detect-adaptive", "threshold", threshold), "weights": self.config.get_value("detect-adaptive", "weights", weights), @@ -903,7 +902,9 @@ def _open_video_stream( param_hint="-i/--input", ) from ex except OSError as ex: - raise click.BadParameter("Input error:\n\n\t%s\n" % str(ex), param_hint="-i/--input") + raise click.BadParameter( + "Input error:\n\n\t%s\n" % str(ex), param_hint="-i/--input" + ) from None def _on_duplicate_command(self, command: str) -> None: """Called when a command is duplicated to stop parsing and raise an error. diff --git a/scenedetect/backends/pyav.py b/scenedetect/backends/pyav.py index 3ca58151..cba203c7 100644 --- a/scenedetect/backends/pyav.py +++ b/scenedetect/backends/pyav.py @@ -14,7 +14,6 @@ from logging import getLogger from typing import AnyStr, BinaryIO, Optional, Tuple, Union -# pylint: disable=c-extension-no-member import av import numpy as np diff --git a/scenedetect/detectors/__init__.py b/scenedetect/detectors/__init__.py index ff8bdd69..a87a5689 100644 --- a/scenedetect/detectors/__init__.py +++ b/scenedetect/detectors/__init__.py @@ -35,9 +35,7 @@ processing videos, however they can also be used to process frames directly. """ -# ruff: noqa: I001 - -from scenedetect.detectors.content_detector import ContentDetector +from scenedetect.detectors.content_detector import ContentDetector # noqa: I001 from scenedetect.detectors.threshold_detector import ThresholdDetector from scenedetect.detectors.adaptive_detector import AdaptiveDetector from scenedetect.detectors.hash_detector import HashDetector diff --git a/scenedetect/platform.py b/scenedetect/platform.py index 54a325c5..65aa7f80 100644 --- a/scenedetect/platform.py +++ b/scenedetect/platform.py @@ -36,7 +36,6 @@ class FakeTqdmObject: """Provides a no-op tqdm-like object.""" - # pylint: disable=unused-argument def __init__(self, **kawrgs): """No-op.""" @@ -49,13 +48,10 @@ def close(self): def set_description(self, desc=None, refresh=True): """No-op.""" - # pylint: enable=unused-argument - class FakeTqdmLoggingRedirect: """Provides a no-op tqdm context manager for redirecting log messages.""" - # pylint: disable=redefined-builtin,unused-argument def __init__(self, **kawrgs): """No-op.""" @@ -65,20 +61,14 @@ def __enter__(self): def __exit__(self, type, value, traceback): """No-op.""" - # pylint: enable=redefined-builtin,unused-argument - # Try to import tqdm and the logging redirect, otherwise provide fake implementations.. try: - # pylint: disable=unused-import from tqdm import tqdm from tqdm.contrib.logging import logging_redirect_tqdm - # pylint: enable=unused-import except ModuleNotFoundError: - # pylint: disable=invalid-name tqdm = FakeTqdmObject logging_redirect_tqdm = FakeTqdmLoggingRedirect - # pylint: enable=invalid-name ## ## OpenCV imwrite Supported Image Types & Quality/Compression Parameters @@ -254,10 +244,8 @@ def get_ffmpeg_path() -> Optional[str]: # Try invoking ffmpeg using the one from `imageio_ffmpeg` if available. try: - # pylint: disable=import-outside-toplevel from imageio_ffmpeg import get_ffmpeg_exe - # pylint: enable=import-outside-toplevel subprocess.call([get_ffmpeg_exe(), "-v", "quiet"]) return get_ffmpeg_exe() # Gracefully handle case where imageio_ffmpeg is not available. diff --git a/scenedetect/scene_detector.py b/scenedetect/scene_detector.py index 316437e6..6ce50993 100644 --- a/scenedetect/scene_detector.py +++ b/scenedetect/scene_detector.py @@ -32,7 +32,6 @@ from scenedetect.stats_manager import StatsManager -# pylint: disable=unused-argument, no-self-use class SceneDetector: """Base class to inherit from when implementing a scene detection algorithm. @@ -184,7 +183,7 @@ def filter(self, frame_num: int, above_threshold: bool) -> ty.List[int]: return self._filter_merge(frame_num=frame_num, above_threshold=above_threshold) elif self._mode == FlashFilter.Mode.SUPPRESS: return self._filter_suppress(frame_num=frame_num, above_threshold=above_threshold) - assert False, "unhandled FlashFilter Mode!" + raise RuntimeError("Unhandled FlashFilter mode.") def _filter_suppress(self, frame_num: int, above_threshold: bool) -> ty.List[int]: min_length_met: bool = (frame_num - self._last_above) >= self._filter_length diff --git a/scenedetect/scene_manager.py b/scenedetect/scene_manager.py index efca1725..dc3bba04 100644 --- a/scenedetect/scene_manager.py +++ b/scenedetect/scene_manager.py @@ -1060,14 +1060,10 @@ def _decode_thread( # Make sure main thread stops processing loop. out_queue.put((None, None)) - # pylint: enable=bare-except - # # Deprecated Methods # - # pylint: disable=unused-argument - def get_cut_list( self, base_timecode: Optional[FrameTimecode] = None, show_warning: bool = True ) -> List[FrameTimecode]: @@ -1117,8 +1113,6 @@ def get_event_list( logger.error("`get_event_list()` is deprecated and will be removed in a future release.") return self._get_event_list() - # pylint: enable=unused-argument - def _is_processing_required(self, frame_num: int) -> bool: """True if frame metrics not in StatsManager, False otherwise.""" if self.stats_manager is None: diff --git a/scenedetect/video_manager.py b/scenedetect/video_manager.py index 5111ade9..ab09c8a5 100644 --- a/scenedetect/video_manager.py +++ b/scenedetect/video_manager.py @@ -287,7 +287,7 @@ def __init__( self, video_files: List[str], framerate: Optional[float] = None, - logger=getLogger("pyscenedetect"), + logger=None, ): """[DEPRECATED] DO NOT USE. @@ -310,6 +310,8 @@ def __init__( """ # TODO(v0.7): Add DeprecationWarning that this class will be removed in v0.8: 'VideoManager # will be removed in PySceneDetect v0.8. Use VideoStreamCv2 or VideoCaptureAdapter instead.' + if logger is None: + logger = getLogger("pyscenedetect") logger.error("VideoManager is deprecated and will be removed.") if not video_files: raise ValueError("At least one string/integer must be passed in the video_files list.") @@ -535,7 +537,6 @@ def start(self) -> None: # This overrides the seek method from the VideoStream interface, but the name was changed # from `timecode` to `target`. For compatibility, we allow calling seek with the form # seek(0), seek(timecode=0), and seek(target=0). Specifying both arguments is an error. - # pylint: disable=arguments-differ def seek(self, timecode: FrameTimecode = None, target: FrameTimecode = None) -> bool: """Seek forwards to the passed timecode. @@ -589,8 +590,6 @@ def seek(self, timecode: FrameTimecode = None, target: FrameTimecode = None) -> return False return True - # pylint: enable=arguments-differ - def release(self) -> None: """Release (cv2.VideoCapture method), releases all open capture(s).""" for cap in self._cap_list: diff --git a/scenedetect/video_stream.py b/scenedetect/video_stream.py index 96642243..8d188daf 100644 --- a/scenedetect/video_stream.py +++ b/scenedetect/video_stream.py @@ -53,7 +53,6 @@ class SeekError(Exception): class VideoOpenFailure(Exception): """Raised by a backend if opening a video fails.""" - # pylint: disable=useless-super-delegation def __init__(self, message: str = "Unknown backend error."): """ Arguments: @@ -61,8 +60,6 @@ def __init__(self, message: str = "Unknown backend error."): """ super().__init__(message) - # pylint: enable=useless-super-delegation - class FrameRateUnavailable(VideoOpenFailure): """Exception instance to provide consistent error messaging across backends when the video frame diff --git a/tests/test_api.py b/tests/test_api.py index 69ede73b..07559253 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -17,8 +17,6 @@ when calling `detect()` or `detect_scenes()`. """ -# pylint: disable=import-outside-toplevel, redefined-outer-name, unused-argument - def test_api_detect(test_video_file: str): """Demonstrate usage of the `detect()` function to process a complete video.""" diff --git a/tests/test_frame_timecode.py b/tests/test_frame_timecode.py index 61b997cc..39b25125 100644 --- a/tests/test_frame_timecode.py +++ b/tests/test_frame_timecode.py @@ -20,9 +20,6 @@ or string HH:MM:SS[.nnn]. timecode format. """ -# ruff: noqa: B015 -# pylint: disable=invalid-name, expression-not-assigned, unneeded-not, pointless-statement - # Third-Party Library Imports import pytest @@ -197,9 +194,9 @@ def test_equality(): assert x != FrameTimecode(timecode=10.0, fps=10.0) # Comparing FrameTimecodes with different framerates raises a TypeError. with pytest.raises(TypeError): - x == FrameTimecode(timecode=1.0, fps=100.0) + assert x == FrameTimecode(timecode=1.0, fps=100.0) with pytest.raises(TypeError): - x == FrameTimecode(timecode=1.0, fps=10.1) + assert x == FrameTimecode(timecode=1.0, fps=10.1) assert x == FrameTimecode(x) assert x == FrameTimecode(1.0, x) @@ -214,17 +211,17 @@ def test_equality(): assert x == 1.0 with pytest.raises(ValueError): - x == "0x" + assert x == "0x" with pytest.raises(ValueError): - x == "x00:00:00.000" + assert x == "x00:00:00.000" with pytest.raises(TypeError): - x == [0] + assert x == [0] with pytest.raises(TypeError): - x == (0,) + assert x == (0,) with pytest.raises(TypeError): - x == [0, 1, 2, 3] + assert x == [0, 1, 2, 3] with pytest.raises(TypeError): - x == {0: 0} + assert x == {0: 0} assert FrameTimecode(timecode="00:00:00.5", fps=10) == "00:00:00.500" assert FrameTimecode(timecode="00:00:01.500", fps=10) == "00:00:01.500" @@ -246,7 +243,7 @@ def test_addition(): assert x + 10 == "00:00:02.000" with pytest.raises(TypeError): - FrameTimecode("00:00:02.000", fps=20.0) == x + 10 + assert FrameTimecode("00:00:02.000", fps=20.0) == x + 10 def test_subtraction(): @@ -265,7 +262,7 @@ def test_subtraction(): assert x - 1 == FrameTimecode(timecode=0.9, fps=10.0) with pytest.raises(TypeError): - FrameTimecode("00:00:02.000", fps=20.0) == x - 10 + assert FrameTimecode("00:00:02.000", fps=20.0) == x - 10 @pytest.mark.parametrize("frame_num,fps", [(1, 1), (61, 14), (29, 25), (126, 24000 / 1001.0)]) diff --git a/tests/test_scene_manager.py b/tests/test_scene_manager.py index 8a1f7f9e..9e19f4c1 100644 --- a/tests/test_scene_manager.py +++ b/tests/test_scene_manager.py @@ -15,8 +15,6 @@ which applies SceneDetector algorithms on VideoStream backends. """ -# pylint: disable=invalid-name - import glob import os import os.path @@ -163,7 +161,6 @@ def test_save_images_zero_width_scene(test_video_file): # TODO: This would be more readable if the callbacks were defined within the test case, e.g. # split up the callback function and callback lambda test cases. -# pylint: disable=unused-argument, unnecessary-lambda class FakeCallback: """Fake callback used for testing. Tracks the frame numbers the callback was invoked with.""" @@ -187,9 +184,6 @@ def _callback(self, image, frame_num): self.scene_list.append(frame_num) -# pylint: enable=unused-argument, unnecessary-lambda - - def test_detect_scenes_callback(test_video_file): """Test SceneManager detect_scenes method with a callback function. diff --git a/tests/test_stats_manager.py b/tests/test_stats_manager.py index 3d8361d2..3701fe5c 100644 --- a/tests/test_stats_manager.py +++ b/tests/test_stats_manager.py @@ -26,8 +26,6 @@ These files will be deleted, if possible, after the tests are completed running. """ -# pylint: disable=protected-access - import csv import os import random diff --git a/tests/test_video_splitter.py b/tests/test_video_splitter.py index a79e3e0c..7fefefbb 100644 --- a/tests/test_video_splitter.py +++ b/tests/test_video_splitter.py @@ -11,8 +11,6 @@ # """Tests for scenedetect.video_splitter module.""" -# pylint: disable=no-self-use,missing-function-docstring - from pathlib import Path import pytest diff --git a/tests/test_video_stream.py b/tests/test_video_stream.py index ccf0b537..c3cc5127 100644 --- a/tests/test_video_stream.py +++ b/tests/test_video_stream.py @@ -16,9 +16,6 @@ all supported backends, and verify that they are functionally equivalent where possible. """ -# ruff: noqa: B011 -# pylint: disable=no-self-use,missing-function-docstring - import os.path from dataclasses import dataclass from typing import List, Type @@ -47,7 +44,7 @@ def calculate_frame_delta(frame_a, frame_b, roi=None) -> float: if roi: - assert False # TODO + raise RuntimeError("TODO") assert frame_a.shape == frame_b.shape num_pixels = frame_a.shape[0] * frame_a.shape[1] return numpy.sum(numpy.abs(frame_b - frame_a)) / num_pixels diff --git a/website/pages/contributing.md b/website/pages/contributing.md index f45b753f..c9662e3d 100644 --- a/website/pages/contributing.md +++ b/website/pages/contributing.md @@ -12,8 +12,8 @@ Development of PySceneDetect happens on [github.com/Breakthrough/PySceneDetect]( The following checklist covers the basics of pre-submission requirements: - Code passes all unit tests (run `pytest`) - - Code is formatted (run `python -m yapf -i -r scenedetect/ tests/` to format in place) - - Generally follows the [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html) + - Code passes static analysis and formatting checks (`ruff check` and `ruff format`) + - Follows the [Google Python Style Guide](https://google.github.io/styleguide/pyguide.html) Note that PySceneDetect is released under the BSD 3-Clause license, and submitted code should comply with this license (see [License & Copyright Information](copyright.md) for details).