Skip to content

Commit 23fc9c5

Browse files
committed
[api] Modernize FrameTimecode type hints
1 parent 504046c commit 23fc9c5

1 file changed

Lines changed: 76 additions & 64 deletions

File tree

scenedetect/common.py

Lines changed: 76 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,8 @@ class FrameTimecode:
179179

180180
def __init__(
181181
self,
182-
timecode: ty.Union[int, float, str, Timecode, "FrameTimecode"] = None,
183-
fps: ty.Union[float, "FrameTimecode", Fraction] = None,
182+
timecode: "int | float | str | Timecode | FrameTimecode",
183+
fps: "float | FrameTimecode | Fraction | None" = None,
184184
):
185185
"""
186186
Arguments:
@@ -194,35 +194,19 @@ def __init__(
194194
"""
195195
self._time: _FrameNumber | _Seconds | Timecode
196196
"""Internal time representation."""
197-
self._rate: Fraction = None
197+
self._rate: Fraction | None = None
198198
"""Rate at which time passes between frames, measured in frames/sec."""
199199

200200
# Copy constructor.
201201
if isinstance(timecode, FrameTimecode):
202-
self._rate = timecode._rate if fps is None else fps
203202
self._time = timecode._time
203+
self._rate = timecode._rate if fps is None else self._ensure_fractional(fps)
204204
return
205205

206-
if not isinstance(fps, (float, Fraction, FrameTimecode)):
207-
raise TypeError("fps must be of type float, Fraction, or FrameTimecode.")
208-
209206
# Ensure args are consistent with API.
210207
if fps is None:
211208
raise TypeError("fps is a required argument.")
212-
if isinstance(fps, FrameTimecode):
213-
self._rate = fps._rate
214-
elif isinstance(fps, float):
215-
if fps <= MAX_FPS_DELTA:
216-
raise ValueError("Framerate must be positive and greater than zero.")
217-
self._rate = Fraction.from_float(fps)
218-
elif isinstance(fps, Fraction):
219-
if float(fps) <= MAX_FPS_DELTA:
220-
raise ValueError("Framerate must be positive and greater than zero.")
221-
self._rate = fps
222-
else:
223-
raise TypeError(
224-
f"Wrong type for fps: {type(fps)} - expected float, Fraction, or FrameTimecode"
225-
)
209+
self._rate = self._ensure_fractional(fps)
226210

227211
# Timecode with a time base.
228212
if isinstance(timecode, Timecode):
@@ -239,12 +223,12 @@ def __init__(
239223
if timecode < 0.0:
240224
raise ValueError("Timecode frame number must be positive and greater than zero.")
241225
self._time = _Seconds(timecode)
242-
elif isinstance(timecode, int):
226+
else:
227+
# Only `int` remains: `Timecode`/`FrameTimecode` returned earlier and `str`/`float`
228+
# were just handled above.
243229
if timecode < 0:
244230
raise ValueError("Timecode frame number must be positive and greater than zero.")
245231
self._time = _FrameNumber(timecode)
246-
else:
247-
raise TypeError("Timecode format/type unrecognized.")
248232

249233
@property
250234
def frame_num(self) -> int:
@@ -274,6 +258,8 @@ def time_base(self) -> Fraction:
274258
"""The time base in which presentation time is calculated."""
275259
if isinstance(self._time, Timecode):
276260
return self._time.time_base
261+
# `_FrameNumber` / `_Seconds` are only assigned after `_rate` is set.
262+
assert self._rate is not None
277263
return 1 / self._rate
278264

279265
@property
@@ -297,7 +283,7 @@ def get_frames(self) -> int:
297283
)
298284
return self.frame_num
299285

300-
def get_framerate(self) -> float:
286+
def get_framerate(self) -> float | None:
301287
"""[DEPRECATED] Get Framerate: Returns the framerate used by the FrameTimecode object.
302288
303289
Use the `framerate` property instead.
@@ -332,6 +318,8 @@ def seconds(self) -> float:
332318
return self._time.seconds
333319
if isinstance(self._time, _Seconds):
334320
return self._time.value
321+
# `_FrameNumber` is only assigned after `_rate` is set.
322+
assert self._rate is not None
335323
return float(self._time.value / self._rate)
336324

337325
def get_seconds(self) -> float:
@@ -404,11 +392,31 @@ def get_timecode(
404392
# Return hours, minutes, and seconds as a formatted timecode string.
405393
return f"{hrs:02d}:{mins:02d}:{secs_str}"
406394

395+
@staticmethod
396+
def _ensure_fractional(fps: "float | FrameTimecode | Fraction") -> Fraction:
397+
"""Validate and convert an `fps` argument into a positive `Fraction`."""
398+
if isinstance(fps, FrameTimecode):
399+
if fps._rate is None:
400+
raise TypeError("FrameTimecode passed as fps must have a known rate.")
401+
return fps._rate
402+
if isinstance(fps, float):
403+
if fps <= MAX_FPS_DELTA:
404+
raise ValueError("Framerate must be positive and greater than zero.")
405+
return Fraction.from_float(fps)
406+
if isinstance(fps, Fraction):
407+
if float(fps) <= MAX_FPS_DELTA:
408+
raise ValueError("Framerate must be positive and greater than zero.")
409+
return fps
410+
raise TypeError(
411+
f"Wrong type for fps: {type(fps)} - expected float, Fraction, or FrameTimecode"
412+
)
413+
407414
def _seconds_to_frames(self, seconds: float) -> int:
408415
"""Convert `seconds` to the nearest number of frames using the current framerate.
409416
410417
*NOTE*: This will not be correct for variable framerate videos.
411418
"""
419+
assert self._rate is not None
412420
return round(seconds * self._rate)
413421

414422
def _parse_timecode_number(self, timecode: int | float) -> int:
@@ -450,7 +458,7 @@ def _timecode_to_seconds(self, input: str) -> float:
450458
timecode = int(input)
451459
if timecode < 0:
452460
raise ValueError("Timecode frame number must be positive.")
453-
return timecode / self.framerate
461+
return timecode / float(self._rate)
454462
# Timecode in string format 'HH:MM:SS[.nnn]' or 'MM:SS[.nnn]'
455463
elif input.find(":") >= 0:
456464
values = input.split(":")
@@ -564,44 +572,46 @@ def __ge__(self, other: ty.Union[int, float, str, "FrameTimecode"]) -> bool:
564572
return self.seconds >= self._get_other_as_seconds(other)
565573
return self.frame_num >= self._get_other_as_frames(other)
566574

567-
def __iadd__(self, other: ty.Union[int, float, str, "FrameTimecode"]) -> "FrameTimecode":
568-
other_is_timecode = isinstance(other, FrameTimecode) and isinstance(other._time, Timecode)
575+
def __iadd__(self, other: "int | float | str | FrameTimecode") -> "FrameTimecode":
576+
# Narrow `other`'s internal time once so pyright can track it through the dispatch below.
577+
other_inner = other._time if isinstance(other, FrameTimecode) else None
569578

570-
if isinstance(self._time, Timecode) and other_is_timecode:
571-
if self._time.time_base == other._time.time_base:
579+
if isinstance(self._time, Timecode) and isinstance(other_inner, Timecode):
580+
if self._time.time_base == other_inner.time_base:
572581
self._time = Timecode(
573-
pts=max(0, self._time.pts + other._time.pts),
582+
pts=max(0, self._time.pts + other_inner.pts),
574583
time_base=self._time.time_base,
575584
)
576585
return self
577586
# Different time bases: use the finer (smaller) one for better precision.
578-
time_base = min(self._time.time_base, other._time.time_base)
587+
time_base = min(self._time.time_base, other_inner.time_base)
579588
self_pts = round(Fraction(self._time.pts) * self._time.time_base / time_base)
580-
other_pts = round(Fraction(other._time.pts) * other._time.time_base / time_base)
589+
other_pts = round(Fraction(other_inner.pts) * other_inner.time_base / time_base)
581590
self._time = Timecode(pts=max(0, self_pts + other_pts), time_base=time_base)
582591
return self
583592

584593
# If either input is a timecode, the output shall also be one. The input which isn't a
585594
# timecode is converted into seconds, after which the equivalent timecode is computed.
586-
if isinstance(self._time, Timecode) or other_is_timecode:
587-
timecode: Timecode = self._time if isinstance(self._time, Timecode) else other._time
588-
seconds: float = (
589-
self._get_other_as_seconds(other)
590-
if isinstance(self._time, Timecode)
591-
else self.seconds
595+
if isinstance(self._time, Timecode):
596+
seconds = self._get_other_as_seconds(other)
597+
self._time = Timecode(
598+
pts=max(0, self._time.pts + round(seconds / self._time.time_base)),
599+
time_base=self._time.time_base,
592600
)
601+
if self._rate is None and isinstance(other, FrameTimecode):
602+
self._rate = other._rate
603+
return self
604+
if isinstance(other_inner, Timecode):
593605
self._time = Timecode(
594-
pts=max(0, timecode.pts + round(seconds / timecode.time_base)),
595-
time_base=timecode.time_base,
606+
pts=max(0, other_inner.pts + round(self.seconds / other_inner.time_base)),
607+
time_base=other_inner.time_base,
596608
)
597-
# Preserve rate if available from self or other.
598609
if self._rate is None and isinstance(other, FrameTimecode):
599610
self._rate = other._rate
600611
return self
601612

602-
other_is_seconds = isinstance(other, FrameTimecode) and isinstance(other._time, _Seconds)
603-
if isinstance(self._time, _Seconds) and other_is_seconds:
604-
self._time = _Seconds(max(0, self._time.value + other._time.value))
613+
if isinstance(self._time, _Seconds) and isinstance(other_inner, _Seconds):
614+
self._time = _Seconds(max(0.0, self._time.value + other_inner.value))
605615
return self
606616

607617
if isinstance(self._time, _Seconds):
@@ -616,44 +626,46 @@ def __add__(self, other: ty.Union[int, float, str, "FrameTimecode"]) -> "FrameTi
616626
to_return += other
617627
return to_return
618628

619-
def __isub__(self, other: ty.Union[int, float, str, "FrameTimecode"]) -> "FrameTimecode":
620-
other_is_timecode = isinstance(other, FrameTimecode) and isinstance(other._time, Timecode)
629+
def __isub__(self, other: "int | float | str | FrameTimecode") -> "FrameTimecode":
630+
# Narrow `other`'s internal time once so pyright can track it through the dispatch below.
631+
other_inner = other._time if isinstance(other, FrameTimecode) else None
621632

622-
if isinstance(self._time, Timecode) and other_is_timecode:
623-
if self._time.time_base == other._time.time_base:
633+
if isinstance(self._time, Timecode) and isinstance(other_inner, Timecode):
634+
if self._time.time_base == other_inner.time_base:
624635
self._time = Timecode(
625-
pts=max(0, self._time.pts - other._time.pts),
636+
pts=max(0, self._time.pts - other_inner.pts),
626637
time_base=self._time.time_base,
627638
)
628639
return self
629640
# Different time bases: use the finer (smaller) one for better precision.
630-
time_base = min(self._time.time_base, other._time.time_base)
641+
time_base = min(self._time.time_base, other_inner.time_base)
631642
self_pts = round(Fraction(self._time.pts) * self._time.time_base / time_base)
632-
other_pts = round(Fraction(other._time.pts) * other._time.time_base / time_base)
643+
other_pts = round(Fraction(other_inner.pts) * other_inner.time_base / time_base)
633644
self._time = Timecode(pts=max(0, self_pts - other_pts), time_base=time_base)
634645
return self
635646

636647
# If either input is a timecode, the output shall also be one. The input which isn't a
637648
# timecode is converted into seconds, after which the equivalent timecode is computed.
638-
if isinstance(self._time, Timecode) or other_is_timecode:
639-
timecode: Timecode = self._time if isinstance(self._time, Timecode) else other._time
640-
seconds: float = (
641-
self._get_other_as_seconds(other)
642-
if isinstance(self._time, Timecode)
643-
else self.seconds
649+
if isinstance(self._time, Timecode):
650+
seconds = self._get_other_as_seconds(other)
651+
self._time = Timecode(
652+
pts=max(0, self._time.pts - round(seconds / self._time.time_base)),
653+
time_base=self._time.time_base,
644654
)
655+
if self._rate is None and isinstance(other, FrameTimecode):
656+
self._rate = other._rate
657+
return self
658+
if isinstance(other_inner, Timecode):
645659
self._time = Timecode(
646-
pts=max(0, timecode.pts - round(seconds / timecode.time_base)),
647-
time_base=timecode.time_base,
660+
pts=max(0, other_inner.pts - round(self.seconds / other_inner.time_base)),
661+
time_base=other_inner.time_base,
648662
)
649-
# Preserve rate if available from self or other.
650663
if self._rate is None and isinstance(other, FrameTimecode):
651664
self._rate = other._rate
652665
return self
653666

654-
other_is_seconds = isinstance(other, FrameTimecode) and isinstance(other._time, _Seconds)
655-
if isinstance(self._time, _Seconds) and other_is_seconds:
656-
self._time = _Seconds(max(0, self._time.value - other._time.value))
667+
if isinstance(self._time, _Seconds) and isinstance(other_inner, _Seconds):
668+
self._time = _Seconds(max(0.0, self._time.value - other_inner.value))
657669
return self
658670

659671
if isinstance(self._time, _Seconds):

0 commit comments

Comments
 (0)