Skip to content

Commit 6d4c780

Browse files
committed
[lint] None checks for backends
1 parent 97666d4 commit 6d4c780

4 files changed

Lines changed: 30 additions & 8 deletions

File tree

scenedetect/backends/moviepy.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def __init__(self, path: ty.AnyStr, framerate: float | None = None, print_infos:
102102
self._frame_number = 0
103103
# We need to manually keep track of EOF as duration may not be accurate.
104104
self._eof = False
105-
self._aspect_ratio: float = None
105+
self._aspect_ratio: float | None = None
106106

107107
#
108108
# VideoStream Methods/Properties
@@ -211,11 +211,13 @@ def seek(self, target: FrameTimecode | float | int):
211211
success = False
212212
if not isinstance(target, FrameTimecode):
213213
target = FrameTimecode(target, self.frame_rate)
214+
duration = self.duration
215+
assert duration is not None
214216
try:
215217
self._last_frame = _retry_on_oserror(
216218
"seek", lambda: self._reader.get_frame(target.seconds)
217219
)
218-
if hasattr(self._reader, "last_read") and target >= self.duration:
220+
if hasattr(self._reader, "last_read") and target >= duration:
219221
raise SeekError("MoviePy > 2.0 does not have proper EOF semantics (#461).")
220222
self._frame_number = min(
221223
target.frame_num,
@@ -228,7 +230,7 @@ def seek(self, target: FrameTimecode | float | int):
228230
#
229231
# We need to ensure consistency for seeking past end of video with respect to errors and
230232
# behaviour, and should probably gracefully stop at the last frame instead of throwing.
231-
if target >= self.duration:
233+
if target >= duration:
232234
raise SeekError("Target frame is beyond end of video!") from ex
233235
raise
234236
finally:
@@ -263,5 +265,6 @@ def read(self, decode: bool = True) -> np.ndarray | bool:
263265
last_frame_valid = self._last_frame is not None and self._last_frame is not False
264266
if last_frame_valid:
265267
self._last_frame_rgb = cv2.cvtColor(self._last_frame, cv2.COLOR_BGR2RGB)
268+
assert self._last_frame_rgb is not None
266269
return self._last_frame_rgb
267270
return not self._eof

scenedetect/backends/opencv.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ def is_seekable(self) -> bool:
178178
@property
179179
def frame_size(self) -> tuple[int, int]:
180180
"""Size of each video frame in pixels as a tuple of (width, height)."""
181+
assert self._cap is not None
181182
return (
182183
math.trunc(self._cap.get(cv2.CAP_PROP_FRAME_WIDTH)),
183184
math.trunc(self._cap.get(cv2.CAP_PROP_FRAME_HEIGHT)),
@@ -188,11 +189,13 @@ def duration(self) -> FrameTimecode | None:
188189
"""Duration of the stream as a FrameTimecode, or None if non terminating."""
189190
if self._is_device:
190191
return None
192+
assert self._cap is not None
191193
return self.base_timecode + math.trunc(self._cap.get(cv2.CAP_PROP_FRAME_COUNT))
192194

193195
@property
194196
def aspect_ratio(self) -> float:
195197
"""Display/pixel aspect ratio as a float (1.0 represents square pixels)."""
198+
assert self._cap is not None
196199
return _get_aspect_ratio(self._cap)
197200

198201
@property
@@ -201,6 +204,7 @@ def timecode(self) -> Timecode:
201204
# *NOTE*: Although OpenCV has `CAP_PROP_PTS`, it doesn't seem to be reliable. For now, we
202205
# use `CAP_PROP_POS_MSEC` instead, converting to microseconds for sufficient precision to
203206
# avoid frame-boundary rounding errors at common framerates like 24000/1001.
207+
assert self._cap is not None
204208
ms = self._cap.get(cv2.CAP_PROP_POS_MSEC)
205209
time_base = Fraction(1, 1000000)
206210
return Timecode(pts=round(ms * 1000), time_base=time_base)
@@ -219,18 +223,22 @@ def position(self) -> FrameTimecode:
219223

220224
@property
221225
def position_ms(self) -> float:
226+
assert self._cap is not None
222227
return self._cap.get(cv2.CAP_PROP_POS_MSEC)
223228

224229
@property
225230
def frame_number(self) -> int:
231+
assert self._cap is not None
226232
return math.trunc(self._cap.get(cv2.CAP_PROP_POS_FRAMES))
227233

228234
def seek(self, target: FrameTimecode | float | int):
229235
if self._is_device:
230236
raise SeekError("Cannot seek if input is a device!")
231237
if target < 0:
232238
raise ValueError("Target seek position cannot be negative!")
239+
assert self._cap is not None
233240

241+
assert self._frame_rate is not None
234242
target_secs = (self.base_timecode + target).seconds
235243
self._has_grabbed = False
236244
if target_secs > 0:
@@ -260,16 +268,20 @@ def seek(self, target: FrameTimecode | float | int):
260268

261269
def reset(self):
262270
"""Close and re-open the VideoStream (should be equivalent to calling `seek(0)`)."""
271+
assert self._cap is not None
272+
assert self._frame_rate is not None
263273
self._cap.release()
264-
self._open_capture(self._frame_rate)
274+
self._open_capture(float(self._frame_rate))
265275

266276
def read(self, decode: bool = True) -> np.ndarray | bool:
277+
assert self._cap is not None
267278
if not self._cap.isOpened():
268279
return False
269280
has_grabbed = self._cap.grab()
270281
# If we failed to grab the frame, retry a few times if required.
271282
if not has_grabbed:
272-
if self.duration > 0 and self.position < (self.duration - 1):
283+
duration = self.duration
284+
if duration is not None and duration > 0 and self.position < (duration - 1):
273285
for _ in range(self._max_decode_attempts):
274286
has_grabbed = self._cap.grab()
275287
if has_grabbed:

scenedetect/backends/pyav.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def __init__(
6868
VideoOpenFailure: video could not be opened (may be corrupted)
6969
ValueError: specified framerate is invalid
7070
"""
71-
self._container = None
71+
self._container: av.container.InputContainer | None = None
7272

7373
# TODO(https://scenedetect.com/issues/258): See what
7474
# `self._container.discard_corrupt = True` does with corrupt videos.
@@ -213,7 +213,7 @@ def rate(self) -> Fraction:
213213
return self._video_stream.guessed_rate
214214

215215
@property
216-
def time_base(self) -> Fraction:
216+
def time_base(self) -> Fraction | None:
217217
if self._frame:
218218
return self._frame.time_base
219219
return None
@@ -265,6 +265,7 @@ def seek(self, target: FrameTimecode | float | int) -> None:
265265
self._frame = None
266266
self._decoder = None
267267
self._decode_count = 0
268+
assert self._container is not None
268269
self._container.seek(target_pts, stream=self._video_stream)
269270
if not beginning:
270271
self.read(decode=False)
@@ -274,6 +275,7 @@ def seek(self, target: FrameTimecode | float | int) -> None:
274275

275276
def reset(self):
276277
"""Close and re-open the VideoStream (should be equivalent to calling `seek(0)`)."""
278+
assert self._container is not None
277279
self._container.close()
278280
self._frame = None
279281
self._decoder = None
@@ -288,9 +290,11 @@ def read(self, decode: bool = True) -> np.ndarray | bool:
288290
# B-frame reordering) is never flushed prematurely. Creating a new generator each call
289291
# caused the last buffered frame to be lost at EOF.
290292
if self._decoder is None:
293+
assert self._container is not None
291294
self._decoder = self._container.decode(video=0)
292295
try:
293296
last_frame = self._frame
297+
assert self._decoder is not None
294298
self._frame = next(self._decoder)
295299
self._decode_count += 1
296300
except av.error.EOFError:
@@ -300,6 +304,7 @@ def read(self, decode: bool = True) -> np.ndarray | bool:
300304
return False
301305
except StopIteration:
302306
return False
307+
assert self._frame is not None
303308
return self._frame.to_ndarray(format="bgr24") if decode else True
304309

305310
#
@@ -309,6 +314,7 @@ def read(self, decode: bool = True) -> np.ndarray | bool:
309314
@property
310315
def _video_stream(self):
311316
"""PyAV `av.video.stream.VideoStream` being used."""
317+
assert self._container is not None
312318
return self._container.streams.video[0]
313319

314320
@property
@@ -365,6 +371,7 @@ def _handle_eof(self):
365371
except:
366372
self._io.seek(orig_pos)
367373
raise
374+
assert self._container is not None
368375
self._container.close()
369376
self._container = container
370377
self._decoder = None

scenedetect/common.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ def __init__(
247247
raise TypeError("Timecode format/type unrecognized.")
248248

249249
@property
250-
def frame_num(self) -> int | None:
250+
def frame_num(self) -> int:
251251
"""The frame number. For VFR video or Timecode-backed objects, this is an approximation
252252
based on the average framerate. Prefer using `pts` and `time_base` for precise timing."""
253253
if isinstance(self._time, Timecode):

0 commit comments

Comments
 (0)