Skip to content

Commit ed6a725

Browse files
committed
[backends] Tighten None safety, reduce unnecessary None checks
1 parent 7f2b25b commit ed6a725

3 files changed

Lines changed: 31 additions & 37 deletions

File tree

pyproject.toml

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,35 @@ unfixable = []
6161

6262
[tool.pyright]
6363
include = ["scenedetect", "tests"]
64+
# Run pyright from inside an activated venv (or pass `--pythonpath` /
65+
# configure your editor's interpreter) so cv2 / av / numpy / moviepy
66+
# resolve. Without this, pyright uses its bundled Python and the report
67+
# fills with cascading "import could not be resolved" noise.
68+
# Per-developer venv paths are deliberately NOT pinned here.
69+
#
70+
# Modes: "off" | "basic" | "standard" | "strict".
71+
# We're at "basic" for 0.7. Bumping requires the cleanup in TODO(0.8) below.
6472
typeCheckingMode = "basic"
65-
# cv2, av, and moviepy ship without type stubs; these reports generate
66-
# unactionable noise without catching real issues in this codebase.
73+
74+
# Third-party noise: cv2, av, moviepy ship without (or with partial) type
75+
# stubs; these reports are unactionable in this codebase.
6776
reportMissingTypeStubs = "none"
6877
reportUnknownMemberType = "none"
6978
reportUnknownArgumentType = "none"
7079
reportUnknownVariableType = "none"
7180
reportUnknownParameterType = "none"
81+
82+
# Click + pytest decorators are conventionally untyped.
83+
reportUntypedFunctionDecorator = "none"
84+
85+
# TODO(0.8): Audit the rules below. They were added globally with a
86+
# third-party justification, but they affect the whole codebase. After the
87+
# remaining first-party type-debt is cleared (FrameTimecode/TimecodeLike in
88+
# common.py, _cli/config.py CropValue overloads, Click CLI typing in
89+
# _cli/__init__.py), re-enable each in isolation and decide keep/drop:
90+
# reportMissingParameterType — first-party coverage looks complete
91+
# reportMissingTypeArgument — modern Python defaults absorb most
92+
# reportPrivateUsage — no cross-module _private access seen
7293
reportMissingParameterType = "none"
7394
reportMissingTypeArgument = "none"
74-
reportUntypedFunctionDecorator = "none"
7595
reportPrivateUsage = "none"

scenedetect/backends/opencv.py

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -119,18 +119,13 @@ def __init__(
119119
self._path_or_device: str | int = resolved
120120
self._is_device = isinstance(self._path_or_device, int)
121121

122-
# Initialized in _open_capture:
123-
self._cap: cv2.VideoCapture | None = (
124-
None # Reference to underlying cv2.VideoCapture object.
125-
)
126-
self._frame_rate: Fraction | None = None
127-
128122
# VideoCapture state
129123
self._has_grabbed = False
130124
self._max_decode_attempts = max_decode_attempts
131125
self._decode_failures = 0
132126
self._warning_displayed = False
133127

128+
# `_open_capture` populates `_cap` and `_frame_rate`.
134129
self._open_capture(framerate)
135130

136131
#
@@ -145,7 +140,6 @@ def capture(self) -> cv2.VideoCapture:
145140
backing this object. Seeking or using the read/grab methods through this property are
146141
unsupported and will leave this object in an inconsistent state.
147142
"""
148-
assert self._cap
149143
return self._cap
150144

151145
#
@@ -157,7 +151,6 @@ def capture(self) -> cv2.VideoCapture:
157151

158152
@property
159153
def frame_rate(self) -> Fraction:
160-
assert self._frame_rate
161154
return self._frame_rate
162155

163156
@property
@@ -189,7 +182,6 @@ def is_seekable(self) -> bool:
189182
@property
190183
def frame_size(self) -> tuple[int, int]:
191184
"""Size of each video frame in pixels as a tuple of (width, height)."""
192-
assert self._cap is not None
193185
return (
194186
math.trunc(self._cap.get(cv2.CAP_PROP_FRAME_WIDTH)),
195187
math.trunc(self._cap.get(cv2.CAP_PROP_FRAME_HEIGHT)),
@@ -200,13 +192,11 @@ def duration(self) -> FrameTimecode | None:
200192
"""Duration of the stream as a FrameTimecode, or None if non terminating."""
201193
if self._is_device:
202194
return None
203-
assert self._cap is not None
204195
return self.base_timecode + math.trunc(self._cap.get(cv2.CAP_PROP_FRAME_COUNT))
205196

206197
@property
207198
def aspect_ratio(self) -> float:
208199
"""Display/pixel aspect ratio as a float (1.0 represents square pixels)."""
209-
assert self._cap is not None
210200
return _get_aspect_ratio(self._cap)
211201

212202
@property
@@ -215,7 +205,6 @@ def timecode(self) -> Timecode:
215205
# *NOTE*: Although OpenCV has `CAP_PROP_PTS`, it doesn't seem to be reliable. For now, we
216206
# use `CAP_PROP_POS_MSEC` instead, converting to microseconds for sufficient precision to
217207
# avoid frame-boundary rounding errors at common framerates like 24000/1001.
218-
assert self._cap is not None
219208
ms = self._cap.get(cv2.CAP_PROP_POS_MSEC)
220209
time_base = Fraction(1, 1000000)
221210
return Timecode(pts=round(ms * 1000), time_base=time_base)
@@ -234,12 +223,10 @@ def position(self) -> FrameTimecode:
234223

235224
@property
236225
def position_ms(self) -> float:
237-
assert self._cap is not None
238226
return self._cap.get(cv2.CAP_PROP_POS_MSEC)
239227

240228
@property
241229
def frame_number(self) -> int:
242-
assert self._cap is not None
243230
return math.trunc(self._cap.get(cv2.CAP_PROP_POS_FRAMES))
244231

245232
def seek(self, target: TimecodeLike):
@@ -249,9 +236,6 @@ def seek(self, target: TimecodeLike):
249236
target = FrameTimecode(target, self.frame_rate)
250237
if target < 0:
251238
raise ValueError("Target seek position cannot be negative!")
252-
assert self._cap is not None
253-
254-
assert self._frame_rate is not None
255239
target_secs = (self.base_timecode + target).seconds
256240
self._has_grabbed = False
257241
if target_secs > 0:
@@ -281,13 +265,10 @@ def seek(self, target: TimecodeLike):
281265

282266
def reset(self):
283267
"""Close and re-open the VideoStream (should be equivalent to calling `seek(0)`)."""
284-
assert self._cap is not None
285-
assert self._frame_rate is not None
286268
self._cap.release()
287269
self._open_capture(float(self._frame_rate))
288270

289271
def read(self, decode: bool = True) -> np.ndarray | bool:
290-
assert self._cap is not None
291272
if not self._cap.isOpened():
292273
return False
293274
has_grabbed = self._cap.grab()
@@ -364,8 +345,8 @@ def _open_capture(self, framerate: float | None = None):
364345
if framerate < MAX_FPS_DELTA:
365346
raise FrameRateUnavailable()
366347

367-
self._cap = cap
368-
self._frame_rate = framerate_to_fraction(framerate)
348+
self._cap: cv2.VideoCapture = cap
349+
self._frame_rate: Fraction = framerate_to_fraction(framerate)
369350
self._has_grabbed = False
370351
cap.set(cv2.CAP_PROP_ORIENTATION_AUTO, 1.0) # https://github.com/opencv/opencv/issues/26795
371352

@@ -430,7 +411,6 @@ def capture(self) -> cv2.VideoCapture:
430411
backing this object. Using the read/grab methods through this property are unsupported and
431412
will leave this object in an inconsistent state.
432413
"""
433-
assert self._cap
434414
return self._cap
435415

436416
#
@@ -443,7 +423,6 @@ def capture(self) -> cv2.VideoCapture:
443423
@property
444424
def frame_rate(self) -> Fraction:
445425
"""Framerate in frames/sec."""
446-
assert self._frame_rate
447426
return self._frame_rate
448427

449428
@property

scenedetect/backends/pyav.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,6 @@ def __init__(
7474
VideoOpenFailure: video could not be opened (may be corrupted)
7575
ValueError: specified framerate is invalid
7676
"""
77-
self._container: av.container.InputContainer | None = None # type: ignore[name-defined]
78-
7977
# TODO(https://scenedetect.com/issues/258): See what
8078
# `self._container.discard_corrupt = True` does with corrupt videos.
8179
super().__init__()
@@ -113,7 +111,7 @@ def __init__(
113111
else:
114112
self._io = path_or_io
115113

116-
self._container = av.open(self._io)
114+
self._container: av.container.InputContainer = av.open(self._io) # type: ignore[attr-defined]
117115
if threading_mode is not None:
118116
self._video_stream.thread_type = threading_mode
119117
self._reopened = False
@@ -145,8 +143,10 @@ def __init__(
145143
self._duration_frames = self._get_duration()
146144

147145
def __del__(self):
148-
if self._container is not None:
149-
self._container.close()
146+
# `_container` is unset if `__init__` raised before `av.open()` succeeded.
147+
container = getattr(self, "_container", None)
148+
if container is not None:
149+
container.close()
150150

151151
#
152152
# VideoStream Methods/Properties
@@ -273,7 +273,6 @@ def seek(self, target: TimecodeLike) -> None:
273273
self._frame = None
274274
self._decoder = None
275275
self._decode_count = 0
276-
assert self._container is not None
277276
self._container.seek(target_pts, stream=self._video_stream)
278277
if not beginning:
279278
self.read(decode=False)
@@ -283,7 +282,6 @@ def seek(self, target: TimecodeLike) -> None:
283282

284283
def reset(self):
285284
"""Close and re-open the VideoStream (should be equivalent to calling `seek(0)`)."""
286-
assert self._container is not None
287285
self._container.close()
288286
self._frame = None
289287
self._decoder = None
@@ -298,7 +296,6 @@ def read(self, decode: bool = True) -> np.ndarray | bool:
298296
# B-frame reordering) is never flushed prematurely. Creating a new generator each call
299297
# caused the last buffered frame to be lost at EOF.
300298
if self._decoder is None:
301-
assert self._container is not None
302299
self._decoder = self._container.decode(video=0)
303300
try:
304301
last_frame = self._frame
@@ -322,7 +319,6 @@ def read(self, decode: bool = True) -> np.ndarray | bool:
322319
@property
323320
def _video_stream(self):
324321
"""PyAV `av.video.stream.VideoStream` being used."""
325-
assert self._container is not None
326322
return self._container.streams.video[0]
327323

328324
@property
@@ -379,7 +375,6 @@ def _handle_eof(self):
379375
except:
380376
self._io.seek(orig_pos)
381377
raise
382-
assert self._container is not None
383378
self._container.close()
384379
self._container = container
385380
self._decoder = None

0 commit comments

Comments
 (0)