Bladeren bron

Add type hints throughout

- consts.py: add FileInfo TypedDict (the shared media file record);
  annotate all Consts class attributes
- proc.py: add ProcResult TypedDict; annotate run() and __init__()
- tag.py: annotate all methods; fix remaining except BaseException and
  bare except: missed in the prior cleanup commit; fix get_int/get_str
  to return None explicitly on decode failure instead of falling through
  with bytes; replace hasattr(value, 'to_bytes') duck-test in set()
  with isinstance(value, int) now that the signature is typed
- probe.py: annotate probe() -> list[dict[str, Any]]
- scan.py: annotate scan(); remove unused imports of Mux and Lang
- mux.py: annotate mux() and internal locals
- lang.py: annotate lang()
- cli.py: annotate main() -> None
- __init__.py: export FileInfo and ProcResult
ZiB 4 maanden geleden
bovenliggende
commit
2814de1b0b

+ 2 - 2
zz_remux/zz_remux/__init__.py

@@ -1,6 +1,6 @@
-from .consts import Consts
+from .consts import Consts, FileInfo
 from .tag import Tag
 from .tag import Tag
 from .scan import Scan
 from .scan import Scan
 from .mux import Mux
 from .mux import Mux
 from .lang import Lang
 from .lang import Lang
-from .proc import Proc
+from .proc import Proc, ProcResult

BIN
zz_remux/zz_remux/__pycache__/__init__.cpython-314.pyc


BIN
zz_remux/zz_remux/__pycache__/consts.cpython-314.pyc


BIN
zz_remux/zz_remux/__pycache__/lang.cpython-314.pyc


BIN
zz_remux/zz_remux/__pycache__/mux.cpython-314.pyc


BIN
zz_remux/zz_remux/__pycache__/probe.cpython-314.pyc


BIN
zz_remux/zz_remux/__pycache__/proc.cpython-314.pyc


BIN
zz_remux/zz_remux/__pycache__/scan.cpython-314.pyc


BIN
zz_remux/zz_remux/__pycache__/tag.cpython-314.pyc


+ 1 - 1
zz_remux/zz_remux/cli.py

@@ -2,7 +2,7 @@
 import re, sys, os, argparse, pathlib, logging
 import re, sys, os, argparse, pathlib, logging
 import zz_remux as remux
 import zz_remux as remux
 
 
-def main():
+def main() -> None:
   # Static config
   # Static config
   log_level = logging.WARN
   log_level = logging.WARN
   prog_name = 'zz_remux'
   prog_name = 'zz_remux'

+ 29 - 16
zz_remux/zz_remux/consts.py

@@ -1,6 +1,19 @@
+import pathlib
+from typing import TypedDict
+
+class FileInfo(TypedDict):
+  path:      pathlib.Path | None
+  name:      str
+  ext:       str
+  group:     str
+  lang:      str | None
+  sdh:       bool
+  mux_vers:  int | None
+  lang_vers: int | None
+
 class Consts:
 class Consts:
   # Default file metadata
   # Default file metadata
-  _default = {
+  _default: FileInfo = {
     'path':      None,
     'path':      None,
     'name':      '',
     'name':      '',
     'ext':       '',
     'ext':       '',
@@ -12,27 +25,27 @@ class Consts:
   }
   }
 
 
   # Parsing constants
   # Parsing constants
-  video_ext     = [ r'mkv', r'mp4', r'm4v' ]
-  subs_ext      = [ r'srt' ]
-  subs_sdh      = [ r'sdh' ]
-  subs_lang     = [ r'\w\w' ]
-  media_ext     = video_ext + subs_ext
+  video_ext:  list[str] = [ r'mkv', r'mp4', r'm4v' ]
+  subs_ext:   list[str] = [ r'srt' ]
+  subs_sdh:   list[str] = [ r'sdh' ]
+  subs_lang:  list[str] = [ r'\w\w' ]
+  media_ext:  list[str] = video_ext + subs_ext
 
 
   # Tag data
   # Tag data
-  mux_tag   = 'zib.mux'
-  mux_vers  = 1
-  lang_tag  = 'zib.lang'
-  lang_vers = 2
+  mux_tag:  str = 'zib.mux'
+  mux_vers: int = 1
+  lang_tag:  str = 'zib.lang'
+  lang_vers: int = 2
 
 
   # Validation size factor
   # Validation size factor
-  min_size = 0.9
+  min_size: float = 0.9
 
 
   # Stream constants
   # Stream constants
-  lang_undef   = 'und'
-  lang_default = 'eng'
-  lang_codecs  = [ 'subtitle', 'audio' ]
-  lang_missing = [ None, lang_undef ]
+  lang_undef:   str            = 'und'
+  lang_default: str            = 'eng'
+  lang_codecs:  list[str]      = [ 'subtitle', 'audio' ]
+  lang_missing: list[str|None] = [ None, lang_undef ]
 
 
   @staticmethod
   @staticmethod
-  def default():
+  def default() -> FileInfo:
     return Consts._default.copy()
     return Consts._default.copy()

+ 8 - 8
zz_remux/zz_remux/lang.py

@@ -1,5 +1,5 @@
-import logging
-from .consts import Consts
+import pathlib, logging
+from .consts import Consts, FileInfo
 from .tag import Tag
 from .tag import Tag
 from .probe import Probe
 from .probe import Probe
 from .proc import Proc
 from .proc import Proc
@@ -7,18 +7,18 @@ from .proc import Proc
 class Lang:
 class Lang:
   sdh_title = 'SDH'
   sdh_title = 'SDH'
 
 
-  def __init__(self, propeditExec='mkvpropedit', logLevel=logging.WARN):
+  def __init__(self, propeditExec: str = 'mkvpropedit', logLevel: int = logging.WARN) -> None:
     # Logging
     # Logging
     self.logger = logging.getLogger(__name__)
     self.logger = logging.getLogger(__name__)
     self.logger.setLevel(logLevel)
     self.logger.setLevel(logLevel)
-    
+
     # Module init
     # Module init
     self.propedit_exec = propeditExec
     self.propedit_exec = propeditExec
     self.tags = Tag(logLevel=logLevel)
     self.tags = Tag(logLevel=logLevel)
     self.probes = Probe(logLevel=logLevel)
     self.probes = Probe(logLevel=logLevel)
     self.proc = Proc(logLevel=logLevel)
     self.proc = Proc(logLevel=logLevel)
 
 
-  def lang(self, media):
+  def lang(self, media: list[FileInfo]) -> None:
     self.logger.info(f"Processing {len(media)} files")
     self.logger.info(f"Processing {len(media)} files")
 
 
     for file in media:
     for file in media:
@@ -41,8 +41,8 @@ class Lang:
 
 
       # Find missing and undefined languages for subtitle and audio streams
       # Find missing and undefined languages for subtitle and audio streams
       # Find hearing_impaired subtitles without SDH title
       # Find hearing_impaired subtitles without SDH title
-      add_lang = []
-      add_sdh  = []
+      add_lang: list[int] = []
+      add_sdh:  list[int] = []
       for stream in streams:
       for stream in streams:
         if stream.get('codec_type') not in Consts.lang_codecs:
         if stream.get('codec_type') not in Consts.lang_codecs:
           self.logger.debug(f"Skipping missing or ignored codec_type: {file['name']}:{stream['index']}")
           self.logger.debug(f"Skipping missing or ignored codec_type: {file['name']}:{stream['index']}")
@@ -62,7 +62,7 @@ class Lang:
       # mkvpropedit
       # mkvpropedit
       if add_lang or add_sdh:
       if add_lang or add_sdh:
         # Remember mkvpropedit starts counting at 1
         # Remember mkvpropedit starts counting at 1
-        args = [ self.propedit_exec, file['path'] ]
+        args: list[str | pathlib.Path] = [ self.propedit_exec, file['path'] ]
         for index in add_lang:
         for index in add_lang:
           self.logger.info(f"Setting lang ({Consts.lang_default}): {file['name']}:{index}")
           self.logger.info(f"Setting lang ({Consts.lang_default}): {file['name']}:{index}")
           args.extend([ '--edit', f"track:{index + 1}", '--set', f"language={Consts.lang_default}" ])
           args.extend([ '--edit', f"track:{index + 1}", '--set', f"language={Consts.lang_default}" ])

+ 10 - 10
zz_remux/zz_remux/mux.py

@@ -1,10 +1,10 @@
 import pathlib, logging
 import pathlib, logging
-from .consts import Consts
+from .consts import Consts, FileInfo
 from .tag import Tag
 from .tag import Tag
 from .proc import Proc
 from .proc import Proc
 
 
 class Mux:
 class Mux:
-  def __init__(self, logLevel=logging.WARN):
+  def __init__(self, logLevel: int = logging.WARN) -> None:
     # Logging
     # Logging
     self.logger = logging.getLogger(__name__)
     self.logger = logging.getLogger(__name__)
     self.logger.setLevel(logLevel)
     self.logger.setLevel(logLevel)
@@ -13,11 +13,11 @@ class Mux:
     self.tags = Tag(logLevel=logLevel)
     self.tags = Tag(logLevel=logLevel)
     self.proc = Proc(logLevel=logLevel)
     self.proc = Proc(logLevel=logLevel)
 
 
-  def mux(self, media):
+  def mux(self, media: list[FileInfo]) -> None:
     self.logger.info(f"Processing {len(media)} files")
     self.logger.info(f"Processing {len(media)} files")
 
 
     # Find groups
     # Find groups
-    groups  = {}
+    groups: dict[str, list[FileInfo]] = {}
     for file in media:
     for file in media:
       if not file['group']:
       if not file['group']:
         self.logger.debug(f"Skipping non-grouped file: {file['name']}")
         self.logger.debug(f"Skipping non-grouped file: {file['name']}")
@@ -39,9 +39,9 @@ class Mux:
           continue
           continue
 
 
       # Find all sources for the group
       # Find all sources for the group
-      size    = 0
-      subs    = []
-      videos  = []
+      size   = 0
+      subs:   list[FileInfo] = []
+      videos: list[FileInfo] = []
       for file in group:
       for file in group:
         if file['ext'] in Consts.video_ext:
         if file['ext'] in Consts.video_ext:
           self.logger.info(f"Found video: {file['name']}")
           self.logger.info(f"Found video: {file['name']}")
@@ -60,9 +60,9 @@ class Mux:
         continue
         continue
 
 
       # Build args
       # Build args
-      final   = videos[0]['path'].parent.joinpath(f'{videos[0]['group']}.mkv')
-      temp    = final.with_suffix('.mkv-remux')
-      args    = [ 'mkvmerge', '--default-language', Consts.lang_default, '-o', temp ]
+      final = videos[0]['path'].parent.joinpath(f'{videos[0]['group']}.mkv')
+      temp  = final.with_suffix('.mkv-remux')
+      args: list[str | pathlib.Path] = [ 'mkvmerge', '--default-language', Consts.lang_default, '-o', temp ]
       args.append(videos[0]['path'])
       args.append(videos[0]['path'])
 
 
       # Subtitles get language and SDH metadata
       # Subtitles get language and SDH metadata

+ 5 - 4
zz_remux/zz_remux/probe.py

@@ -1,10 +1,11 @@
-import json, logging
+import json, logging, pathlib
+from typing import Any
 from .proc import Proc
 from .proc import Proc
 
 
 class Probe:
 class Probe:
   ffprobe_exec = 'ffprobe'
   ffprobe_exec = 'ffprobe'
-  
-  def __init__(self, ffprobe_exec='ffprobe', logLevel=logging.WARN):
+
+  def __init__(self, ffprobe_exec: str = 'ffprobe', logLevel: int = logging.WARN) -> None:
     self.logger = logging.getLogger(__name__)
     self.logger = logging.getLogger(__name__)
     self.logger.setLevel(logLevel)
     self.logger.setLevel(logLevel)
 
 
@@ -12,7 +13,7 @@ class Probe:
     self.ffprobe_exec = ffprobe_exec
     self.ffprobe_exec = ffprobe_exec
     self.proc = Proc(logLevel=logLevel)
     self.proc = Proc(logLevel=logLevel)
 
 
-  def probe(self, path):
+  def probe(self, path: pathlib.Path) -> list[dict[str, Any]]:
     self.logger.debug(f"Probing: {path}")
     self.logger.debug(f"Probing: {path}")
 
 
     # Sanity
     # Sanity

+ 10 - 4
zz_remux/zz_remux/proc.py

@@ -1,18 +1,24 @@
-import subprocess, logging
+import subprocess, logging, pathlib
+from typing import TypedDict
+
+class ProcResult(TypedDict):
+  out:  str | None
+  err:  str | None
+  code: int
 
 
 class Proc:
 class Proc:
   encoding = 'utf-8'
   encoding = 'utf-8'
 
 
-  def __init__(self, stdout=subprocess.PIPE, stderr=subprocess.PIPE, logLevel=logging.WARN):
+  def __init__(self, stdout=subprocess.PIPE, stderr=subprocess.PIPE, logLevel: int = logging.WARN) -> None:
     self.logger = logging.getLogger(__name__)
     self.logger = logging.getLogger(__name__)
     self.logger.setLevel(logLevel)
     self.logger.setLevel(logLevel)
 
 
     self.stdout = stdout
     self.stdout = stdout
     self.stderr = stderr
     self.stderr = stderr
 
 
-  def run(self, cmd):
+  def run(self, cmd: list[str | pathlib.Path]) -> ProcResult:
     # Return dict
     # Return dict
-    ret = {'out': None, 'err': None, 'code': int(-111) }
+    ret: ProcResult = {'out': None, 'err': None, 'code': int(-111) }
 
 
     # Sanity
     # Sanity
     if not cmd or len(cmd) < 1:
     if not cmd or len(cmd) < 1:

+ 6 - 8
zz_remux/zz_remux/scan.py

@@ -1,12 +1,10 @@
 import re, pathlib, logging
 import re, pathlib, logging
 import langcodes
 import langcodes
-from .consts import Consts
+from .consts import Consts, FileInfo
 from .tag import Tag
 from .tag import Tag
-from .mux import Mux
-from .lang import Lang
 
 
-class Scan:  
-  def __init__(self, logLevel=logging.WARN):
+class Scan:
+  def __init__(self, logLevel: int = logging.WARN) -> None:
     # Logging
     # Logging
     self.logger = logging.getLogger(__name__)
     self.logger = logging.getLogger(__name__)
     self.logger.setLevel(logLevel)
     self.logger.setLevel(logLevel)
@@ -24,14 +22,14 @@ class Scan:
       re.IGNORECASE
       re.IGNORECASE
     )
     )
 
 
-  def scan(self, files, parent=pathlib.Path(''), match=None):
+  def scan(self, files: list[str], parent: pathlib.Path = pathlib.Path(''), match: re.Pattern[str] | None = None) -> list[FileInfo]:
     self.logger.info(f"Processing {len(files)} files")
     self.logger.info(f"Processing {len(files)} files")
-    media = []
+    media: list[FileInfo] = []
 
 
     # Scan this directory for media
     # Scan this directory for media
     for file in files:
     for file in files:
       self.logger.info(f"Found file: {file}")
       self.logger.info(f"Found file: {file}")
-      
+
       info = Consts.default()
       info = Consts.default()
       info['path'] = parent.joinpath(file)
       info['path'] = parent.joinpath(file)
       info['name'] = file
       info['name'] = file

+ 18 - 18
zz_remux/zz_remux/tag.py

@@ -5,70 +5,70 @@ class Tag:
   # Internals
   # Internals
   _encoding = 'utf-8'
   _encoding = 'utf-8'
 
 
-  def __init__(self, logLevel=logging.WARN):
+  def __init__(self, logLevel: int = logging.WARN) -> None:
     self.logger = logging.getLogger(__name__)
     self.logger = logging.getLogger(__name__)
     self.logger.setLevel(logLevel)
     self.logger.setLevel(logLevel)
 
 
-  def get_int(self, path, attr):
+  def get_int(self, path: pathlib.Path, attr: str) -> int | None:
     ret = self.get_bytes(path, attr)
     ret = self.get_bytes(path, attr)
     if ret:
     if ret:
       try:
       try:
-        ret = int.from_bytes(ret)
+        return int.from_bytes(ret)
       except Exception:
       except Exception:
         self.logger.error(f"Unable to decode int ({path}): {attr} => {ret}")
         self.logger.error(f"Unable to decode int ({path}): {attr} => {ret}")
-    return ret
+    return None
 
 
-  def get_str(self, path, attr):
+  def get_str(self, path: pathlib.Path, attr: str) -> str | None:
     ret = self.get_bytes(path, attr)
     ret = self.get_bytes(path, attr)
     if ret:
     if ret:
       try:
       try:
-        ret = ret.decode(self._encoding)
+        return ret.decode(self._encoding)
       except Exception:
       except Exception:
         self.logger.error(f"Unable to decode str ({path}): {attr} => {ret}")
         self.logger.error(f"Unable to decode str ({path}): {attr} => {ret}")
-    return ret
+    return None
 
 
-  def get(self, path, attr):
+  def get(self, path: pathlib.Path, attr: str) -> str | None:
     return self.get_str(path, attr)
     return self.get_str(path, attr)
 
 
-  def get_bytes(self, path, attr):
+  def get_bytes(self, path: pathlib.Path, attr: str) -> bytes | None:
     value = None
     value = None
     try:
     try:
       if attr in xattr.listxattr(path):
       if attr in xattr.listxattr(path):
         value = xattr.getxattr(path, attr)
         value = xattr.getxattr(path, attr)
         self.logger.debug(f"Got tag ({path}): {attr} => {value}")
         self.logger.debug(f"Got tag ({path}): {attr} => {value}")
-    except BaseException as e:
+    except Exception as e:
       self.logger.error(f"Get error ({path}:{attr}): {e}")
       self.logger.error(f"Get error ({path}:{attr}): {e}")
     return value
     return value
 
 
-  def set_bytes(self, path, attr, value):
+  def set_bytes(self, path: pathlib.Path, attr: str, value: bytes) -> bool:
     try:
     try:
       xattr.setxattr(path, attr, value)
       xattr.setxattr(path, attr, value)
       self.logger.debug(f"Wrote tag ({path}): {attr} => {value}")
       self.logger.debug(f"Wrote tag ({path}): {attr} => {value}")
       return True
       return True
-    except BaseException as e:
+    except Exception as e:
       self.logger.error(f"Set error ({path}:{attr}): {e}")
       self.logger.error(f"Set error ({path}:{attr}): {e}")
       return False
       return False
 
 
-  def set_int(self, path, attr, value):
+  def set_int(self, path: pathlib.Path, attr: str, value: int) -> bool:
     try:
     try:
       bval = value.to_bytes()
       bval = value.to_bytes()
       return self.set_bytes(path, attr, bval)
       return self.set_bytes(path, attr, bval)
-    except:
+    except Exception:
       self.logger.error(f"Unable to encode int: {value}")
       self.logger.error(f"Unable to encode int: {value}")
       return False
       return False
 
 
-  def set_str(self, path, attr, value):
+  def set_str(self, path: pathlib.Path, attr: str, value: str) -> bool:
     try:
     try:
       bval = value.encode(self._encoding)
       bval = value.encode(self._encoding)
       return self.set_bytes(path, attr, bval)
       return self.set_bytes(path, attr, bval)
-    except:
+    except Exception:
       self.logger.error(f"Unable to encode str: {value}")
       self.logger.error(f"Unable to encode str: {value}")
       return False
       return False
 
 
-  def set(self, path, attr, value):
+  def set(self, path: pathlib.Path, attr: str, value: bytes | int | str) -> bool:
     if isinstance(value, bytes):
     if isinstance(value, bytes):
       return self.set_bytes(path, attr, value)
       return self.set_bytes(path, attr, value)
-    elif hasattr(value, 'to_bytes'):
+    elif isinstance(value, int):
       return self.set_int(path, attr, value)
       return self.set_int(path, attr, value)
     elif isinstance(value, str):
     elif isinstance(value, str):
       return self.set_str(path, attr, value)
       return self.set_str(path, attr, value)