From 2f301ac5e43f738f06f6893b32d7d0172b865ff5 Mon Sep 17 00:00:00 2001 From: Austin Witherspoon Date: Mon, 18 Mar 2024 10:08:15 -0700 Subject: [PATCH 1/5] Add type hints --- setup.py | 3 + timecode/__init__.py | 169 ++++++++++++++++++++++++++++--------------- timecode/py.typed | 0 3 files changed, 114 insertions(+), 58 deletions(-) create mode 100644 timecode/py.typed diff --git a/setup.py b/setup.py index 8c262ab..9e6daf9 100644 --- a/setup.py +++ b/setup.py @@ -59,5 +59,8 @@ def get_meta(meta): keywords=['video', 'timecode', 'smpte'], packages=find_packages(), include_package_data=True, + package_data={ + "timecode": ["py.typed"], + }, zip_safe=True, ) diff --git a/timecode/__init__.py b/timecode/__init__.py index e656f60..eb4754a 100644 --- a/timecode/__init__.py +++ b/timecode/__init__.py @@ -29,6 +29,16 @@ __url__ = "https://github.com/eoyilmaz/timecode" +from fractions import Fraction +import sys +from typing import Optional, Tuple, Union, overload + +try: + from typing import Literal +except ImportError: + pass + + class Timecode(object): """The main timecode class. @@ -56,15 +66,18 @@ class Timecode(object): force_non_drop_frame (bool): If True, uses Non-Dropframe calculation for 29.97 or 59.94 only. Has no meaning for any other framerate. It is False by default. - """ + """ + _framerate: Union[str, int, float, Fraction] + _int_framerate: int + _frames: int def __init__( self, - framerate, - start_timecode=None, - start_seconds=None, - frames=None, - force_non_drop_frame=False, + framerate: Union[str, int, float, Fraction], + start_timecode: Optional[str] = None, + start_seconds: Optional[Union[int, float]] = None, + frames: Optional[int] = None, + force_non_drop_frame: bool = False, ): self.force_non_drop_frame = force_non_drop_frame @@ -73,11 +86,11 @@ def __init__( self.ms_frame = False self.fraction_frame = False - self._int_framerate = None - self._framerate = None - self.framerate = framerate + self._int_framerate = None # type: ignore + self._framerate = None # type: ignore + self.framerate = framerate # type: ignore - self._frames = None + self._frames = None # type: ignore # attribute override order # start_timecode > frames > start_seconds @@ -95,16 +108,16 @@ def __init__( self.frames = self.tc_to_frames("00:00:00:00") @property - def frames(self): + def frames(self) -> int: """Return the _frames attribute value. Returns: int: The frames attribute value. """ - return self._frames + return self._frames # type: ignore @frames.setter - def frames(self, frames): + def frames(self, frames: int) -> None: """Set the_frames attribute. Args: @@ -127,16 +140,16 @@ def frames(self, frames): self._frames = frames @property - def framerate(self): + def framerate(self) -> str: """Return the _framerate attribute. Returns: str: The frame rate of this Timecode instance. """ - return self._framerate + return self._framerate # type: ignore @framerate.setter - def framerate(self, framerate): + def framerate(self, framerate: Union[int, float, str, Tuple[int, int], Fraction]) -> None: """Set the framerate attribute. Args: @@ -158,8 +171,8 @@ def framerate(self, framerate): denominator = getattr(framerate, 'denominator', None) try: - if "/" in framerate: - numerator, denominator = framerate.split("/") + if "/" in framerate: # type: ignore + numerator, denominator = framerate.split("/") # type: ignore except TypeError: # not a string pass @@ -187,7 +200,7 @@ def framerate(self, framerate): self._int_framerate = 60 self.drop_frame = not self.force_non_drop_frame self._ntsc_framerate = True - elif any(map(lambda x: framerate.startswith(x), ["23.976", "23.98"])): + elif any(map(lambda x: framerate.startswith(x), ["23.976", "23.98"])): # type: ignore self._int_framerate = 24 self._ntsc_framerate = True elif framerate in ["ms", "1000"]: @@ -197,11 +210,11 @@ def framerate(self, framerate): elif framerate == "frames": self._int_framerate = 1 else: - self._int_framerate = int(float(framerate)) + self._int_framerate = int(float(framerate)) # type: ignore - self._framerate = framerate + self._framerate = framerate # type: ignore - def set_fractional(self, state): + def set_fractional(self, state: bool) -> None: """Set if the Timecode is to be represented with fractional seconds. Args: @@ -211,7 +224,7 @@ def set_fractional(self, state): """ self.fraction_frame = state - def set_timecode(self, timecode): + def set_timecode(self, timecode: Union[str, "Timecode"]) -> None: """Set the frames by using the given timecode. Args: @@ -220,7 +233,7 @@ def set_timecode(self, timecode): """ self.frames = self.tc_to_frames(timecode) - def float_to_tc(self, seconds): + def float_to_tc(self, seconds: float) -> int: """Return the number of frames in the given seconds using the current instance. Args: @@ -232,7 +245,7 @@ def float_to_tc(self, seconds): """ return int(seconds * self._int_framerate) - def tc_to_frames(self, timecode): + def tc_to_frames(self, timecode: Union[str, "Timecode"]) -> int: """Convert the given Timecode to frames. Args: @@ -258,7 +271,7 @@ def tc_to_frames(self, timecode): if self.framerate != "frames": ffps = float(self.framerate) else: - ffps = float(self._int_framerate) + ffps = float(self._int_framerate) if self.drop_frame: # Number of drop frames is 6% of framerate rounded to nearest @@ -296,14 +309,14 @@ def tc_to_frames(self, timecode): return frame_number + 1 # frames - def frames_to_tc(self, frames, skip_rollover = False): + def frames_to_tc(self, frames: int, skip_rollover: bool = False) -> Tuple[int, int, int, Union[float, int]]: """Convert frames back to timecode. Args: frames (int): Number of frames. Returns: - str: The string representation of the current Timecode instance. + tuple: A tuple containing the hours, minutes, seconds and frames """ if self.drop_frame: # Number of frames to drop on the minute marks is the nearest @@ -343,9 +356,9 @@ def frames_to_tc(self, frames, skip_rollover = False): ifps = self._int_framerate - frs = frame_number % ifps + frs: Union[int, float] = frame_number % ifps if self.fraction_frame: - frs = round(frs / float(ifps), 3) + frs = round(frs / float(ifps), 3) secs = int((frame_number // ifps) % 60) mins = int(((frame_number // ifps) // 60) % 60) @@ -353,14 +366,14 @@ def frames_to_tc(self, frames, skip_rollover = False): return hrs, mins, secs, frs - def tc_to_string(self, hrs, mins, secs, frs): + def tc_to_string(self, hrs: int, mins: int, secs: int, frs: Union[float, int]) -> str: """Return the string representation of a Timecode with given info. Args: hrs (int): The hours portion of the Timecode. mins (int): The minutes portion of the Timecode. secs (int): The seconds portion of the Timecode. - frs (int): The frames portion of the Timecode. + frs (int | float): The frames portion of the Timecode. Returns: str: The string representation of this Timecode.ßß @@ -376,7 +389,18 @@ def tc_to_string(self, hrs, mins, secs, frs): hrs, mins, secs, self.frame_delimiter, frs ) - def to_systemtime(self, as_float=False): + # to maintain python 3.7 compatibility (no literal type yet!) + # only use overload in 3.8+ + if sys.version_info >= (3, 8): + @overload + def to_systemtime(self, as_float: Literal[True]) -> float: + pass + + @overload + def to_systemtime(self, as_float: Literal[False]) -> str: + pass + + def to_systemtime(self, as_float: bool = False) -> Union[str, float]: # type:ignore """Convert a Timecode to the video system timestamp. For NTSC rates, the video system time is not the wall-clock one. @@ -396,7 +420,18 @@ def to_systemtime(self, as_float=False): return (hh*3600 + mm*60 + ss + ms) return "{:02d}:{:02d}:{:02d}.{:03d}".format(hh, mm, ss, round(ms*1000)) - def to_realtime(self, as_float=False): + # to maintain python 3.7 compatibility (no literal type yet!) + # only use typing.Literal in 3.8+ + if sys.version_info >= (3, 8): + @overload # type: ignore # noqa + def to_realtime(self, as_float: Literal[True]) -> float: + pass + + @overload # type: ignore # noqa + def to_realtime(self, as_float: Literal[False]) -> str: + pass + + def to_realtime(self, as_float: bool = False) -> Union[str, float]: # type:ignore """Convert a Timecode to a "real time" timestamp. Reference: SMPTE 12-1 §5.1.2 @@ -427,7 +462,7 @@ def to_realtime(self, as_float=False): return "{:02d}:{:02d}:{:02d}.{:03d}".format(hh, mm, ss, ms) @classmethod - def parse_timecode(cls, timecode): + def parse_timecode(cls, timecode: Union[int, str]) -> Tuple[int, int, int, int]: """Parse the given timecode string. This uses the frame separator do decide if this is a NDF, DF or a @@ -465,7 +500,7 @@ def parse_timecode(cls, timecode): return hrs, mins, secs, frs @property - def frame_delimiter(self): + def frame_delimiter(self) -> str: """Return correct frame deliminator symbol based on the framerate. Returns: @@ -489,7 +524,7 @@ def __iter__(self): """ yield self - def next(self): + def next(self) -> "Timecode": """Add one frame to this Timecode to go the next frame. Returns: @@ -499,7 +534,7 @@ def next(self): self.add_frames(1) return self - def back(self): + def back(self) -> "Timecode": """Subtract one frame from this Timecode to go back one frame. Returns: @@ -509,7 +544,7 @@ def back(self): self.sub_frames(1) return self - def add_frames(self, frames): + def add_frames(self, frames: int) -> None: """Add or subtract frames from the number of frames of this Timecode. Args: @@ -518,7 +553,7 @@ def add_frames(self, frames): """ self.frames += frames - def sub_frames(self, frames): + def sub_frames(self, frames: int) -> None: """Add or subtract frames from the number of frames of this Timecode. Args: @@ -527,7 +562,7 @@ def sub_frames(self, frames): """ self.add_frames(-frames) - def mult_frames(self, frames): + def mult_frames(self, frames: int) -> None: """Multiply frames. Args: @@ -535,7 +570,7 @@ def mult_frames(self, frames): """ self.frames *= frames - def div_frames(self, frames): + def div_frames(self, frames: int) -> None: """Divide the number of frames to the given number. Args: @@ -544,7 +579,7 @@ def div_frames(self, frames): """ self.frames = int(self.frames / frames) - def __eq__(self, other): + def __eq__(self, other: Union[int, str, "Timecode", object]) -> bool: """Override the equality operator. Args: @@ -563,8 +598,10 @@ def __eq__(self, other): return self.__eq__(new_tc) elif isinstance(other, int): return self.frames == other + else: + return False - def __ge__(self, other): + def __ge__(self, other: Union[int, str, "Timecode", object]) -> bool: """Override greater than or equal to operator. Args: @@ -583,8 +620,12 @@ def __ge__(self, other): return self.frames >= new_tc.frames elif isinstance(other, int): return self.frames >= other + else: + raise TypeError( + "'>=' not supported between instances of 'Timecode' and '{}'".format(other.__class__.__name__) + ) - def __gt__(self, other): + def __gt__(self, other: Union[int, str, "Timecode"]) -> bool: """Override greater than operator. Args: @@ -603,8 +644,12 @@ def __gt__(self, other): return self.frames > new_tc.frames elif isinstance(other, int): return self.frames > other + else: + raise TypeError( + "'>' not supported between instances of 'Timecode' and '{}'".format(other.__class__.__name__) + ) - def __le__(self, other): + def __le__(self, other: Union[int, str, "Timecode", object]) -> bool: """Override less or equal to operator. Args: @@ -623,8 +668,12 @@ def __le__(self, other): return self.frames <= new_tc.frames elif isinstance(other, int): return self.frames <= other + else: + raise TypeError( + "'<' not supported between instances of 'Timecode' and '{}'".format(other.__class__.__name__) + ) - def __lt__(self, other): + def __lt__(self, other: Union[int, str, "Timecode"]) -> bool: """Override less than operator. Args: @@ -643,8 +692,12 @@ def __lt__(self, other): return self.frames < new_tc.frames elif isinstance(other, int): return self.frames < other + else: + raise TypeError( + "'<=' not supported between instances of 'Timecode' and '{}'".format(other.__class__.__name__) + ) - def __add__(self, other): + def __add__(self, other: Union["Timecode", int]) -> "Timecode": """Return a new Timecode with the given timecode or frames added to this one. Args: @@ -672,7 +725,7 @@ def __add__(self, other): return tc - def __sub__(self, other): + def __sub__(self, other: Union["Timecode", int]) -> "Timecode": """Return a new Timecode instance with subtracted value. Args: @@ -697,7 +750,7 @@ def __sub__(self, other): tc.drop_frame = self.drop_frame return tc - def __mul__(self, other): + def __mul__(self, other: Union["Timecode", int]) -> "Timecode": """Return a new Timecode instance with multiplied value. Args: @@ -722,7 +775,7 @@ def __mul__(self, other): tc.drop_frame = self.drop_frame return tc - def __div__(self, other): + def __div__(self, other: Union["Timecode", int]) -> "Timecode": """Return a new Timecode instance with divided value. Args: @@ -746,7 +799,7 @@ def __div__(self, other): return Timecode(self.framerate, frames=div_frames) - def __truediv__(self, other): + def __truediv__(self, other: Union["Timecode", int]) -> "Timecode": """Return a new Timecode instance with divided value. Args: @@ -767,7 +820,7 @@ def __repr__(self): return self.tc_to_string(*self.frames_to_tc(self.frames)) @property - def hrs(self): + def hrs(self) -> int: """Return the hours part of the timecode. Returns: @@ -777,7 +830,7 @@ def hrs(self): return hrs @property - def mins(self): + def mins(self) -> int: """Return the minutes part of the timecode. Returns: @@ -787,7 +840,7 @@ def mins(self): return mins @property - def secs(self): + def secs(self) -> int: """Return the seconds part of the timecode. Returns: @@ -797,7 +850,7 @@ def secs(self): return secs @property - def frs(self): + def frs(self) -> Union[float, int]: """Return the frames part of the timecode. Returns: @@ -807,7 +860,7 @@ def frs(self): return frs @property - def frame_number(self): + def frame_number(self) -> int: """Return the 0-based frame number of the current timecode instance. Returns: @@ -816,7 +869,7 @@ def frame_number(self): return self.frames - 1 @property - def float(self): + def float(self) -> float: """Return the seconds as float. Returns: diff --git a/timecode/py.typed b/timecode/py.typed new file mode 100644 index 0000000..e69de29 From d6cf2880a32bf6f9c470b492a139d9ef6ac45309 Mon Sep 17 00:00:00 2001 From: Austin Witherspoon Date: Mon, 18 Mar 2024 10:11:45 -0700 Subject: [PATCH 2/5] Set minimum python version to 3.7 --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 9e6daf9..12bd252 100644 --- a/setup.py +++ b/setup.py @@ -62,5 +62,6 @@ def get_meta(meta): package_data={ "timecode": ["py.typed"], }, + python_requires=">=3.7", zip_safe=True, ) From f835443d5e81aab36b66aa83dd28d1dadc4ecad8 Mon Sep 17 00:00:00 2001 From: Erkan Ozgur Yilmaz Date: Mon, 8 Jul 2024 12:07:34 +0100 Subject: [PATCH 3/5] [#55] Fix encoding issue in `setup.py`. --- setup.py | 2 +- timecode/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 12bd252..66655f9 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ def read_file(file_path): Returns: str: The file content. """ - with open(file_path) as f: + with open(file_path, encoding="utf-8") as f: data = f.read() return data diff --git a/timecode/__init__.py b/timecode/__init__.py index eb4754a..8ead589 100644 --- a/timecode/__init__.py +++ b/timecode/__init__.py @@ -22,7 +22,7 @@ # THE SOFTWARE. __name__ = "timecode" -__version__ = "1.4.0" +__version__ = "1.4.1" __description__ = "SMPTE Time Code Manipulation Library" __author__ = "Erkan Ozgur Yilmaz" __author_email__ = "eoyilmaz@gmail.com" From 9377b6bfb496d12c74153108aa831ab7bd40b90e Mon Sep 17 00:00:00 2001 From: Erkan Ozgur Yilmaz Date: Mon, 8 Jul 2024 12:29:55 +0100 Subject: [PATCH 4/5] [#53] Added `.venv` to `.gitignore`. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c13425e..5d00ebc 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ .coverage *.egg-info/* *.swp +.venv/* docs/html/* docs/latex/* docs/doctrees/* From 8f3c39d6dacb5f79e1a8e6336b8b846f0fe7d485 Mon Sep 17 00:00:00 2001 From: Erkan Ozgur Yilmaz Date: Mon, 8 Jul 2024 12:30:41 +0100 Subject: [PATCH 5/5] [#53] Removed the class variable-esque type definitions from the `Timecode` class. --- timecode/__init__.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/timecode/__init__.py b/timecode/__init__.py index 8ead589..bd4e5f2 100644 --- a/timecode/__init__.py +++ b/timecode/__init__.py @@ -66,10 +66,7 @@ class Timecode(object): force_non_drop_frame (bool): If True, uses Non-Dropframe calculation for 29.97 or 59.94 only. Has no meaning for any other framerate. It is False by default. - """ - _framerate: Union[str, int, float, Fraction] - _int_framerate: int - _frames: int + """ def __init__( self, @@ -86,11 +83,10 @@ def __init__( self.ms_frame = False self.fraction_frame = False - self._int_framerate = None # type: ignore - self._framerate = None # type: ignore + self._int_framerate : Union[None, int] = None + self._framerate : Union[None, str, int, float, Fraction] = None self.framerate = framerate # type: ignore - - self._frames = None # type: ignore + self._frames : Union[None, int] = None # attribute override order # start_timecode > frames > start_seconds