@@ -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