diff --git a/pysubs2/__init__.py b/pysubs2/__init__.py index 492c15a..a141bdb 100644 --- a/pysubs2/__init__.py +++ b/pysubs2/__init__.py @@ -3,7 +3,7 @@ from .ssafile import SSAFile from .ssaevent import SSAEvent from .ssastyle import SSAStyle -from . import time, formats, cli, whisper +from . import time, formats, cli, whisper, exceptions from .exceptions import * from .common import Color, Alignment, VERSION @@ -18,3 +18,20 @@ #: Alias for `pysubs2.common.VERSION`. __version__ = VERSION + +__all__ = [ + "SSAFile", + "SSAEvent", + "SSAStyle", + "time", + "formats", + "cli", + "whisper", + "exceptions", + "Color", + "Alignment", + "VERSION", + "load", + "load_from_whisper", + "make_time", +] diff --git a/pysubs2/cli.py b/pysubs2/cli.py index 149227e..9e4c2d7 100644 --- a/pysubs2/cli.py +++ b/pysubs2/cli.py @@ -7,6 +7,8 @@ from io import open import sys from textwrap import dedent +from typing import List + from .formats import get_file_extension, FORMAT_IDENTIFIERS from .time import make_time from .ssafile import SSAFile @@ -42,7 +44,7 @@ def change_ext(path: str, ext: str) -> str: class Pysubs2CLI: - def __init__(self): + def __init__(self) -> None: parser = self.parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, prog="pysubs2", description=dedent(""" @@ -116,15 +118,16 @@ def __init__(self): extra_sub_options.add_argument("--sub-no-write-fps-declaration", action="store_true", help="(output) omit writing FPS as first zero-length subtitle") - def __call__(self, argv): + def __call__(self, argv: List[str]) -> int: try: - self.main(argv) + return self.main(argv) except KeyboardInterrupt: - exit("\nAborted by user.") + print("\nAborted by user.", file=sys.stderr) + return 1 - def main(self, argv): + def main(self, argv: List[str]) -> int: # Dealing with empty arguments - if argv == []: + if not argv: argv = ["--help"] args = self.parser.parse_args(argv) @@ -169,10 +172,12 @@ def main(self, argv): if args.output_format is None: outpath = path output_format = subs.format + assert output_format is not None, "subs.format must not be None (it was read from file)" else: ext = get_file_extension(args.output_format) outpath = change_ext(path, ext) output_format = args.output_format + assert output_format is not None, "args.output_format must not be None (see if/else)" if args.output_dir is not None: _, filename = op.split(outpath) @@ -188,12 +193,13 @@ def main(self, argv): subs = SSAFile.from_file(infile, args.input_format, args.fps) self.process(subs, args) output_format = args.output_format or subs.format + assert output_format is not None, "output_format must not be None (it's either given or inferred at read time)" subs.to_file(outfile, output_format, args.fps, apply_styles=not args.clean) - return (0 if errors == 0 else 1) + return 0 if errors == 0 else 1 @staticmethod - def process(subs, args): + def process(subs: SSAFile, args: argparse.Namespace) -> None: if args.shift is not None: subs.shift(ms=args.shift) elif args.shift_back is not None: @@ -206,7 +212,7 @@ def process(subs, args): subs.remove_miscellaneous_events() -def __main__(): +def __main__() -> None: cli = Pysubs2CLI() rv = cli(sys.argv[1:]) sys.exit(rv) diff --git a/pysubs2/common.py b/pysubs2/common.py index 8595f48..e41b816 100644 --- a/pysubs2/common.py +++ b/pysubs2/common.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import Union +from typing import Tuple, Union from enum import IntEnum @@ -54,7 +54,7 @@ def to_ssa_alignment(self) -> int: return SSA_ALIGNMENT[self.value - 1] -SSA_ALIGNMENT = (1, 2, 3, 9, 10, 11, 5, 6, 7) +SSA_ALIGNMENT: Tuple[int, ...] = (1, 2, 3, 9, 10, 11, 5, 6, 7) #: Version of the pysubs2 library. diff --git a/pysubs2/formatbase.py b/pysubs2/formatbase.py index 6ea3ea7..fcb2bee 100644 --- a/pysubs2/formatbase.py +++ b/pysubs2/formatbase.py @@ -1,6 +1,6 @@ -from typing import Optional -import io -import pysubs2 +from typing import Optional, TYPE_CHECKING, Any, TextIO +if TYPE_CHECKING: + from .ssafile import SSAFile class FormatBase: @@ -19,7 +19,7 @@ class FormatBase: """ @classmethod - def from_file(cls, subs: "pysubs2.SSAFile", fp: io.TextIOBase, format_: str, **kwargs): + def from_file(cls, subs: "SSAFile", fp: TextIO, format_: str, **kwargs: Any) -> None: """ Load subtitle file into an empty SSAFile. @@ -42,7 +42,7 @@ def from_file(cls, subs: "pysubs2.SSAFile", fp: io.TextIOBase, format_: str, **k raise NotImplementedError("Parsing is not supported for this format") @classmethod - def to_file(cls, subs: "pysubs2.SSAFile", fp: io.TextIOBase, format_: str, **kwargs): + def to_file(cls, subs: "SSAFile", fp: TextIO, format_: str, **kwargs: Any) -> None: """ Write SSAFile into a file. diff --git a/pysubs2/jsonformat.py b/pysubs2/jsonformat.py index 37ded3e..654534c 100644 --- a/pysubs2/jsonformat.py +++ b/pysubs2/jsonformat.py @@ -1,15 +1,19 @@ import dataclasses import json +from typing import Any, Optional, TextIO, TYPE_CHECKING + from .common import Color from .ssaevent import SSAEvent from .ssastyle import SSAStyle from .formatbase import FormatBase +if TYPE_CHECKING: + from .ssafile import SSAFile # We're using Color dataclass # https://stackoverflow.com/questions/51286748/make-the-python-json-encoder-support-pythons-new-dataclasses class EnhancedJSONEncoder(json.JSONEncoder): - def default(self, o): + def default(self, o: Any) -> Any: if dataclasses.is_dataclass(o): return dataclasses.asdict(o) return super().default(o) @@ -22,13 +26,15 @@ class JSONFormat(FormatBase): This is essentially SubStation Alpha as JSON. """ @classmethod - def guess_format(cls, text): + def guess_format(cls, text: str) -> Optional[str]: """See :meth:`pysubs2.formats.FormatBase.guess_format()`""" if text.startswith("{\"") and "\"info:\"" in text: return "json" + else: + return None @classmethod - def from_file(cls, subs, fp, format_, **kwargs): + def from_file(cls, subs: "SSAFile", fp: TextIO, format_: str, **kwargs: Any) -> None: """See :meth:`pysubs2.formats.FormatBase.from_file()`""" data = json.load(fp) @@ -47,7 +53,7 @@ def from_file(cls, subs, fp, format_, **kwargs): subs.events = [SSAEvent(**fields) for fields in data["events"]] @classmethod - def to_file(cls, subs, fp, format_, **kwargs): + def to_file(cls, subs: "SSAFile", fp: TextIO, format_: str, **kwargs: Any) -> None: """See :meth:`pysubs2.formats.FormatBase.to_file()`""" data = { "info": dict(**subs.info), diff --git a/pysubs2/microdvd.py b/pysubs2/microdvd.py index 9fbcc39..3c7abc6 100644 --- a/pysubs2/microdvd.py +++ b/pysubs2/microdvd.py @@ -1,11 +1,16 @@ from functools import partial import re +from typing import Optional, TextIO, Any, TYPE_CHECKING + from .exceptions import UnknownFPSError from .ssaevent import SSAEvent from .ssastyle import SSAStyle from .formatbase import FormatBase from .substation import parse_tags from .time import ms_to_frames, frames_to_ms +if TYPE_CHECKING: + from .ssafile import SSAFile + #: Matches a MicroDVD line. MICRODVD_LINE = re.compile(r" *\{ *(\d+) *\} *\{ *(\d+) *\}(.+)") @@ -14,13 +19,16 @@ class MicroDVDFormat(FormatBase): """MicroDVD subtitle format implementation""" @classmethod - def guess_format(cls, text): + def guess_format(cls, text: str) -> Optional[str]: """See :meth:`pysubs2.formats.FormatBase.guess_format()`""" if any(map(MICRODVD_LINE.match, text.splitlines())): return "microdvd" + else: + return None @classmethod - def from_file(cls, subs, fp, format_, fps=None, strict_fps_inference: bool = True, **kwargs): + def from_file(cls, subs: "SSAFile", fp: TextIO, format_: str, fps: Optional[float] = None, + strict_fps_inference: bool = True, **kwargs: Any) -> None: """ See :meth:`pysubs2.formats.FormatBase.from_file()` @@ -60,10 +68,10 @@ def from_file(cls, subs, fp, format_, fps=None, strict_fps_inference: bool = Tru start, end = map(partial(frames_to_ms, fps=fps), (fstart, fend)) - def prepare_text(text): + def prepare_text(text: str) -> str: text = text.replace("|", r"\N") - def style_replacer(match: re.Match) -> str: + def style_replacer(match: re.Match[str]) -> str: tags = [c for c in "biu" if c in match.group(0)] return "{%s}" % "".join(f"\\{c}1" for c in tags) @@ -78,7 +86,8 @@ def style_replacer(match: re.Match) -> str: subs.append(ev) @classmethod - def to_file(cls, subs, fp, format_, fps=None, write_fps_declaration=True, apply_styles=True, **kwargs): + def to_file(cls, subs: "SSAFile", fp: TextIO, format_: str, fps: Optional[float] = None, + write_fps_declaration: bool = True, apply_styles: bool = True, **kwargs: Any) -> None: """ See :meth:`pysubs2.formats.FormatBase.to_file()` diff --git a/pysubs2/mpl2.py b/pysubs2/mpl2.py index bc98c67..2f401e4 100644 --- a/pysubs2/mpl2.py +++ b/pysubs2/mpl2.py @@ -1,8 +1,10 @@ import re - +from typing import Optional, Any, TextIO, TYPE_CHECKING from .time import times_to_ms from .formatbase import FormatBase from .ssaevent import SSAEvent +if TYPE_CHECKING: + from .ssafile import SSAFile # thanks to http://otsaloma.io/gaupol/doc/api/aeidon.files.mpl2_source.html @@ -12,13 +14,15 @@ class MPL2Format(FormatBase): """MPL2 subtitle format implementation""" @classmethod - def guess_format(cls, text): + def guess_format(cls, text: str) -> Optional[str]: """See :meth:`pysubs2.formats.FormatBase.guess_format()`""" if MPL2_FORMAT.search(text): return "mpl2" + else: + return None @classmethod - def from_file(cls, subs, fp, format_, **kwargs): + def from_file(cls, subs: "SSAFile", fp: TextIO, format_: str, **kwargs: Any) -> None: """See :meth:`pysubs2.formats.FormatBase.from_file()`""" def prepare_text(lines: str) -> str: out = [] @@ -42,7 +46,7 @@ def prepare_text(lines: str) -> str: subs.append(e) @classmethod - def to_file(cls, subs, fp, format_, **kwargs): + def to_file(cls, subs: "SSAFile", fp: TextIO, format_: str, **kwargs: Any) -> None: """ See :meth:`pysubs2.formats.FormatBase.to_file()` diff --git a/pysubs2/ssaevent.py b/pysubs2/ssaevent.py index 06a1a34..a6cbff9 100644 --- a/pysubs2/ssaevent.py +++ b/pysubs2/ssaevent.py @@ -1,6 +1,6 @@ import re import warnings -from typing import Optional, Dict, Any, ClassVar +from typing import Optional, Dict, Any, ClassVar, FrozenSet import dataclasses from .common import IntOrFloat @@ -41,7 +41,7 @@ class SSAEvent: type: str = "Dialogue" #: Line type (Dialogue/Comment) @property - def FIELDS(self): + def FIELDS(self) -> FrozenSet[str]: """All fields in SSAEvent.""" warnings.warn("Deprecated in 1.2.0 - it's a dataclass now", DeprecationWarning) return frozenset(field.name for field in dataclasses.fields(self)) @@ -57,7 +57,7 @@ def duration(self) -> IntOrFloat: return self.end - self.start @duration.setter - def duration(self, ms: int): + def duration(self, ms: int) -> None: if ms >= 0: self.end = self.start + ms else: @@ -74,7 +74,7 @@ def is_comment(self) -> bool: return self.type == "Comment" @is_comment.setter - def is_comment(self, value: bool): + def is_comment(self, value: bool) -> None: if value: self.type = "Comment" else: @@ -111,11 +111,11 @@ def plaintext(self) -> str: return text @plaintext.setter - def plaintext(self, text: str): + def plaintext(self, text: str) -> None: self.text = text.replace("\n", r"\N") - def shift(self, h: IntOrFloat=0, m: IntOrFloat=0, s: IntOrFloat=0, ms: IntOrFloat=0, - frames: Optional[int]=None, fps: Optional[float]=None): + def shift(self, h: IntOrFloat = 0, m: IntOrFloat = 0, s: IntOrFloat = 0, ms: IntOrFloat = 0, + frames: Optional[int] = None, fps: Optional[float] = None) -> None: """ Shift start and end times. @@ -141,36 +141,36 @@ def equals(self, other: "SSAEvent") -> bool: else: raise TypeError("Cannot compare to non-SSAEvent object") - def __eq__(self, other) -> bool: + def __eq__(self, other: object) -> bool: # XXX document this if not isinstance(other, SSAEvent): return NotImplemented return self.start == other.start and self.end == other.end - def __ne__(self, other) -> bool: + def __ne__(self, other: object) -> bool: if not isinstance(other, SSAEvent): return NotImplemented return self.start != other.start or self.end != other.end - def __lt__(self, other) -> bool: + def __lt__(self, other: object) -> bool: if not isinstance(other, SSAEvent): return NotImplemented return (self.start, self.end) < (other.start, other.end) - def __le__(self, other) -> bool: + def __le__(self, other: object) -> bool: if not isinstance(other, SSAEvent): return NotImplemented return (self.start, self.end) <= (other.start, other.end) - def __gt__(self, other) -> bool: + def __gt__(self, other: object) -> bool: if not isinstance(other, SSAEvent): return NotImplemented return (self.start, self.end) > (other.start, other.end) - def __ge__(self, other) -> bool: + def __ge__(self, other: object) -> bool: if not isinstance(other, SSAEvent): return NotImplemented return (self.start, self.end) >= (other.start, other.end) - def __repr__(self): + def __repr__(self) -> str: return f"" diff --git a/pysubs2/ssafile.py b/pysubs2/ssafile.py index ccbe9c9..da44d00 100644 --- a/pysubs2/ssafile.py +++ b/pysubs2/ssafile.py @@ -4,7 +4,7 @@ from itertools import chain import os.path import logging -from typing import Optional, List, Dict, Iterable, Any, overload, Iterator +from typing import Optional, List, Dict, Iterable, Any, overload, Iterator, TextIO, Tuple from .common import IntOrFloat from .formats import autodetect_format, get_format_class, get_format_identifier @@ -14,7 +14,7 @@ from .time import make_time, ms_to_str -class SSAFile(MutableSequence): +class SSAFile(MutableSequence[SSAEvent]): """ Subtitle file in SubStation Alpha format. @@ -32,7 +32,7 @@ class SSAFile(MutableSequence): """ - DEFAULT_INFO = { + DEFAULT_INFO: Dict[str, str] = { "WrapStyle": "0", "ScaledBorderAndShadow": "yes", "Collisions": "Normal" @@ -53,7 +53,8 @@ def __init__(self) -> None: # ------------------------------------------------------------------------ @classmethod - def load(cls, path: str, encoding: str="utf-8", format_: Optional[str]=None, fps: Optional[float]=None, **kwargs) -> "SSAFile": + def load(cls, path: str, encoding: str = "utf-8", format_: Optional[str] = None, fps: Optional[float] = None, + **kwargs: Any) -> "SSAFile": """ Load subtitle file from given path. @@ -104,7 +105,8 @@ def load(cls, path: str, encoding: str="utf-8", format_: Optional[str]=None, fps return cls.from_file(fp, format_, fps=fps, **kwargs) @classmethod - def from_string(cls, string: str, format_: Optional[str]=None, fps: Optional[float]=None, **kwargs) -> "SSAFile": + def from_string(cls, string: str, format_: Optional[str] = None, fps: Optional[float] = None, + **kwargs: Any) -> "SSAFile": """ Load subtitle file from string. @@ -112,6 +114,15 @@ def from_string(cls, string: str, format_: Optional[str]=None, fps: Optional[flo Arguments: string (str): Subtitle file in a string. Note that the string must be Unicode (``str``, not ``bytes``). + format_ (str): Optional, forces use of specific parser + (eg. `"srt"`, `"ass"`). Otherwise, format is detected + automatically from file contents. This argument should + be rarely needed. + fps (float): Framerate for frame-based formats (MicroDVD), + for other formats this argument is ignored. Framerate might + be detected from the file, in which case you don't need + to specify it here (when given, this argument overrides + autodetection). Returns: SSAFile @@ -129,7 +140,8 @@ def from_string(cls, string: str, format_: Optional[str]=None, fps: Optional[flo return cls.from_file(fp, format_, fps=fps, **kwargs) @classmethod - def from_file(cls, fp: io.TextIOBase, format_: Optional[str]=None, fps: Optional[float]=None, **kwargs) -> "SSAFile": + def from_file(cls, fp: TextIO, format_: Optional[str] = None, fps: Optional[float] = None, + **kwargs: Any) -> "SSAFile": """ Read subtitle file from file object. @@ -142,6 +154,15 @@ def from_file(cls, fp: io.TextIOBase, format_: Optional[str]=None, fps: Optional Arguments: fp (file object): A file object, ie. :class:`io.TextIOBase` instance. Note that the file must be opened in text mode (as opposed to binary). + format_ (str): Optional, forces use of specific parser + (eg. `"srt"`, `"ass"`). Otherwise, format is detected + automatically from file contents. This argument should + be rarely needed. + fps (float): Framerate for frame-based formats (MicroDVD), + for other formats this argument is ignored. Framerate might + be detected from the file, in which case you don't need + to specify it here (when given, this argument overrides + autodetection). Returns: SSAFile @@ -163,7 +184,8 @@ def from_file(cls, fp: io.TextIOBase, format_: Optional[str]=None, fps: Optional impl.from_file(subs, fp, format_, fps=fps, **kwargs) return subs - def save(self, path: str, encoding: str="utf-8", format_: Optional[str]=None, fps: Optional[float]=None, **kwargs): + def save(self, path: str, encoding: str = "utf-8", format_: Optional[str] = None, fps: Optional[float] = None, + **kwargs: Any) -> None: """ Save subtitle file to given path. @@ -207,7 +229,7 @@ def save(self, path: str, encoding: str="utf-8", format_: Optional[str]=None, fp with open(path, "w", encoding=encoding) as fp: self.to_file(fp, format_, fps=fps, **kwargs) - def to_string(self, format_: str, fps: Optional[float]=None, **kwargs) -> str: + def to_string(self, format_: str, fps: Optional[float] = None, **kwargs: Any) -> str: """ Get subtitle file as a string. @@ -221,7 +243,7 @@ def to_string(self, format_: str, fps: Optional[float]=None, **kwargs) -> str: self.to_file(fp, format_, fps=fps, **kwargs) return fp.getvalue() - def to_file(self, fp: io.TextIOBase, format_: str, fps: Optional[float]=None, **kwargs): + def to_file(self, fp: TextIO, format_: str, fps: Optional[float] = None, **kwargs: Any) -> None: """ Write subtitle file to file object. @@ -243,8 +265,8 @@ def to_file(self, fp: io.TextIOBase, format_: str, fps: Optional[float]=None, ** # Retiming subtitles # ------------------------------------------------------------------------ - def shift(self, h: IntOrFloat=0, m: IntOrFloat=0, s: IntOrFloat=0, ms: IntOrFloat=0, - frames: Optional[int]=None, fps: Optional[float]=None): + def shift(self, h: IntOrFloat = 0, m: IntOrFloat = 0, s: IntOrFloat = 0, ms: IntOrFloat = 0, + frames: Optional[int] = None, fps: Optional[float] = None) -> None: """ Shift all subtitles by constant time amount. @@ -252,7 +274,10 @@ def shift(self, h: IntOrFloat=0, m: IntOrFloat=0, s: IntOrFloat=0, ms: IntOrFloa case, specify both frames and fps. h, m, s, ms will be ignored. Arguments: - h, m, s, ms: Integer or float values, may be positive or negative. + h: Integer or float values, may be positive or negative (hours). + m: Integer or float values, may be positive or negative (minutes). + s: Integer or float values, may be positive or negative (seconds). + ms: Integer or float values, may be positive or negative (milliseconds). frames (int): When specified, must be an integer number of frames. May be positive or negative. fps must be also specified. fps (float): When specified, must be a positive number. @@ -266,7 +291,7 @@ def shift(self, h: IntOrFloat=0, m: IntOrFloat=0, s: IntOrFloat=0, ms: IntOrFloa line.start += delta line.end += delta - def transform_framerate(self, in_fps: float, out_fps: float): + def transform_framerate(self, in_fps: float, out_fps: float) -> None: """ Rescale all timestamps by ratio of in_fps/out_fps. @@ -293,7 +318,7 @@ def transform_framerate(self, in_fps: float, out_fps: float): # Working with styles # ------------------------------------------------------------------------ - def rename_style(self, old_name: str, new_name: str): + def rename_style(self, old_name: str, new_name: str) -> None: """ Rename a style, including references to it. @@ -322,7 +347,7 @@ def rename_style(self, old_name: str, new_name: str): if line.style == old_name: line.style = new_name - def import_styles(self, subs: "SSAFile", overwrite: bool=True): + def import_styles(self, subs: "SSAFile", overwrite: bool = True) -> None: """ Merge in styles from other SSAFile. @@ -343,7 +368,7 @@ def import_styles(self, subs: "SSAFile", overwrite: bool=True): # Helper methods # ------------------------------------------------------------------------ - def remove_miscellaneous_events(self): + def remove_miscellaneous_events(self) -> None: """ Remove subtitles which appear to be non-essential (the --clean in CLI) @@ -356,7 +381,7 @@ def remove_miscellaneous_events(self): new_events = [] duplicate_text_ids = set() - times_to_texts = {} + times_to_texts: Dict[Tuple[int, int], List[str]] = {} for i, e in enumerate(self): tmp = times_to_texts.setdefault((e.start, e.end), []) if tmp.count(e.plaintext) > 0: @@ -381,7 +406,7 @@ def get_text_events(self) -> List[SSAEvent]: """ return [e for e in self if e.is_text] - def equals(self, other: "SSAFile"): + def equals(self, other: "SSAFile") -> bool: """ Equality of two SSAFiles. @@ -461,7 +486,7 @@ def equals(self, other: "SSAFile"): else: raise TypeError("Cannot compare to non-SSAFile object") - def __repr__(self): + def __repr__(self) -> str: if self.events: max_time = max(ev.end for ev in self) s = f"" @@ -474,7 +499,7 @@ def __repr__(self): # MutableSequence implementation + sort() # ------------------------------------------------------------------------ - def sort(self): + def sort(self) -> None: """Sort subtitles time-wise, in-place.""" self.events.sort() @@ -483,24 +508,24 @@ def __iter__(self) -> Iterator[SSAEvent]: @overload def __getitem__(self, item: int) -> SSAEvent: - return self.events[item] + pass @overload def __getitem__(self, s: slice) -> List[SSAEvent]: - return self.events[s] + pass - def __getitem__(self, item): + def __getitem__(self, item: Any) -> Any: return self.events[item] @overload - def __setitem__(self, key: int, value: SSAEvent): + def __setitem__(self, key: int, value: SSAEvent) -> None: pass @overload - def __setitem__(self, keys: slice, values: Iterable[SSAEvent]): + def __setitem__(self, keys: slice, values: Iterable[SSAEvent]) -> None: pass - def __setitem__(self, key, value): + def __setitem__(self, key: Any, value: Any) -> None: if isinstance(key, int): if isinstance(value, SSAEvent): self.events[key] = value @@ -516,20 +541,20 @@ def __setitem__(self, key, value): raise TypeError("Bad key type") @overload - def __delitem__(self, key: int): + def __delitem__(self, key: int) -> None: pass @overload - def __delitem__(self, s: slice): + def __delitem__(self, s: slice) -> None: pass - def __delitem__(self, key): + def __delitem__(self, key: Any) -> None: del self.events[key] - def __len__(self): + def __len__(self) -> int: return len(self.events) - def insert(self, index: int, value: SSAEvent): + def insert(self, index: int, value: SSAEvent) -> None: if isinstance(value, SSAEvent): self.events.insert(index, value) else: diff --git a/pysubs2/ssastyle.py b/pysubs2/ssastyle.py index f224761..b0df571 100644 --- a/pysubs2/ssastyle.py +++ b/pysubs2/ssastyle.py @@ -1,5 +1,5 @@ import warnings -from typing import Dict, Any, ClassVar +from typing import Dict, Any, ClassVar, FrozenSet import dataclasses from .common import Color, Alignment @@ -23,7 +23,7 @@ class SSAStyle: DEFAULT_STYLE: ClassVar["SSAStyle"] = None # type: ignore[assignment] @property - def FIELDS(self): + def FIELDS(self) -> FrozenSet[str]: """All fields in SSAStyle.""" warnings.warn("Deprecated in 1.2.0 - it's a dataclass now", DeprecationWarning) return frozenset(field.name for field in dataclasses.fields(self)) @@ -66,7 +66,7 @@ def as_dict(self) -> Dict[str, Any]: # dataclasses.asdict() would recursively dictify Color objects, which we don't want return {field.name: getattr(self, field.name) for field in dataclasses.fields(self)} - def __repr__(self): + def __repr__(self) -> str: return f" str: return f"{h:02d}:{m:02d}:{s:02d},{ms:03d}" @staticmethod - def timestamp_to_ms(groups): + def timestamp_to_ms(groups: Sequence[str]) -> int: return timestamp_to_ms(groups) @classmethod - def guess_format(cls, text): + def guess_format(cls, text: str) -> Optional[str]: """See :meth:`pysubs2.formats.FormatBase.guess_format()`""" if "[Script Info]" in text or "[V4+ Styles]" in text: # disambiguation vs. SSA/ASS @@ -47,8 +49,11 @@ def guess_format(cls, text): if len(cls.TIMESTAMP.findall(line)) == 2: return "srt" + return None + @classmethod - def from_file(cls, subs, fp, format_, keep_html_tags=False, keep_unknown_html_tags=False, **kwargs): + def from_file(cls, subs: "SSAFile", fp: TextIO, format_: str, keep_html_tags: bool = False, + keep_unknown_html_tags: bool = False, **kwargs: Any) -> None: """ See :meth:`pysubs2.formats.FormatBase.from_file()` @@ -69,8 +74,8 @@ def from_file(cls, subs, fp, format_, keep_html_tags=False, keep_unknown_html_ta If False, these other HTML tags will be stripped from output (in the previous example, you would get only ``example {\\i1}text{\\i0}``). """ - timestamps = [] # (start, end) - following_lines = [] # contains lists of lines following each timestamp + timestamps: List[Tuple[int, int]] = [] # (start, end) + following_lines: List[List[str]] = [] # contains lists of lines following each timestamp for line in fp: stamps = cls.TIMESTAMP.findall(line) @@ -82,7 +87,7 @@ def from_file(cls, subs, fp, format_, keep_html_tags=False, keep_unknown_html_ta if timestamps: following_lines[-1].append(line) - def prepare_text(lines): + def prepare_text(lines: List[str]) -> str: # Handle the "happy" empty subtitle case, which is timestamp line followed by blank line(s) # followed by number line and timestamp line of the next subtitle. Fixes issue #11. if (len(lines) >= 2 @@ -107,11 +112,13 @@ def prepare_text(lines): s = re.sub(r"\n", r"\\N", s) # convert newlines return s - subs.events = [SSAEvent(start=start, end=end, text=prepare_text(lines)) - for (start, end), lines in zip(timestamps, following_lines)] + for (start, end), lines in zip(timestamps, following_lines): + e = SSAEvent(start=start, end=end, text=prepare_text(lines)) + subs.append(e) @classmethod - def to_file(cls, subs, fp, format_, apply_styles=True, keep_ssa_tags=False, **kwargs): + def to_file(cls, subs: "SSAFile", fp: TextIO, format_: str, apply_styles: bool = True, + keep_ssa_tags: bool = False, **kwargs: Any) -> None: """ See :meth:`pysubs2.formats.FormatBase.to_file()` @@ -133,7 +140,7 @@ def to_file(cls, subs, fp, format_, apply_styles=True, keep_ssa_tags=False, **kw is SRT which doesn't use line styles - this shouldn't be much of an issue in practice.) """ - def prepare_text(text: str, style: SSAStyle): + def prepare_text(text: str, style: SSAStyle) -> str: text = text.replace(r"\h", " ") text = text.replace(r"\n", "\n") text = text.replace(r"\N", "\n") @@ -165,5 +172,5 @@ def prepare_text(text: str, style: SSAStyle): lineno += 1 @classmethod - def _get_visible_lines(cls, subs: "pysubs2.SSAFile") -> List["pysubs2.SSAEvent"]: + def _get_visible_lines(cls, subs: "SSAFile") -> List[SSAEvent]: return subs.get_text_events() diff --git a/pysubs2/substation.py b/pysubs2/substation.py index 4123f4c..6513b26 100644 --- a/pysubs2/substation.py +++ b/pysubs2/substation.py @@ -2,21 +2,22 @@ import re import warnings from numbers import Number -from typing import Any, Union, Optional, Dict +from typing import Any, Union, Optional, Dict, Tuple, List, TextIO, TYPE_CHECKING -import pysubs2 from .formatbase import FormatBase from .ssaevent import SSAEvent from .ssastyle import SSAStyle from .common import Color, Alignment, SSA_ALIGNMENT from .time import make_time, ms_to_times, timestamp_to_ms, TIMESTAMP, TIMESTAMP_SHORT +if TYPE_CHECKING: + from .ssafile import SSAFile -def ass_to_ssa_alignment(i): +def ass_to_ssa_alignment(i: int) -> int: warnings.warn("ass_to_ssa_alignment function is deprecated, please use the Alignment enum", DeprecationWarning) return SSA_ALIGNMENT[i-1] -def ssa_to_ass_alignment(i): +def ssa_to_ass_alignment(i: int) -> int: warnings.warn("ssa_to_ass_alignment function is deprecated, please use the Alignment enum", DeprecationWarning) return SSA_ALIGNMENT.index(i) + 1 @@ -86,7 +87,8 @@ def is_valid_field_content(s: str) -> bool: return "\n" not in s and "," not in s -def parse_tags(text: str, style: SSAStyle = SSAStyle.DEFAULT_STYLE, styles: Optional[Dict[str, SSAStyle]] = None): +def parse_tags(text: str, style: SSAStyle = SSAStyle.DEFAULT_STYLE, + styles: Optional[Dict[str, SSAStyle]] = None) -> List[Tuple[str, SSAStyle]]: """ Split text into fragments with computed SSAStyles. @@ -117,9 +119,9 @@ def apply_overrides(all_overrides: str) -> SSAStyle: s = style.copy() # reset to original line style elif tag.startswith(r"\r"): name = tag[2:] - if name in styles: # type: ignore[operator] + if name in styles: # reset to named style - s = styles[name].copy() # type: ignore[index] + s = styles[name].copy() else: if "i" in tag: s.italic = "1" in tag @@ -166,18 +168,20 @@ def ms_to_timestamp(requested_ms: int) -> str: return f"{h:01d}:{m:02d}:{s:02d}.{cs:02d}" @classmethod - def guess_format(cls, text): + def guess_format(cls, text: str) -> Optional[str]: """See :meth:`pysubs2.formats.FormatBase.guess_format()`""" if re.search(r"V4\+ Styles", text, re.IGNORECASE): return "ass" elif re.search(r"V4 Styles", text, re.IGNORECASE): return "ssa" + else: + return None @classmethod - def from_file(cls, subs: "pysubs2.SSAFile", fp, format_, **kwargs): + def from_file(cls, subs: "SSAFile", fp: TextIO, format_: str, **kwargs: Any) -> None: """See :meth:`pysubs2.formats.FormatBase.from_file()`""" - def string_to_field(f: str, v: str): + def string_to_field(f: str, v: str) -> Any: # Per issue #45, we should handle the case where there is extra whitespace around the values. # Extra whitespace is removed in non-string fields where it would break the parser otherwise, # and in font name (where it doesn't really make sense). It is preserved in Dialogue string @@ -316,7 +320,7 @@ def string_to_field(f: str, v: str): current_attachment_name = None @classmethod - def to_file(cls, subs: "pysubs2.SSAFile", fp, format_, header_notice=NOTICE, **kwargs): + def to_file(cls, subs: "SSAFile", fp: TextIO, format_: str, header_notice: str = NOTICE, **kwargs: Any) -> None: """See :meth:`pysubs2.formats.FormatBase.to_file()`""" print("[Script Info]", file=fp) for line in header_notice.splitlines(False): @@ -331,7 +335,7 @@ def to_file(cls, subs: "pysubs2.SSAFile", fp, format_, header_notice=NOTICE, **k for k, v in subs.aegisub_project.items(): print(k, v, sep=": ", file=fp) - def field_to_string(f: str, v: Any, line: Union[SSAEvent, SSAStyle]): + def field_to_string(f: str, v: Any, line: Union[SSAEvent, SSAStyle]) -> str: if f in {"start", "end"}: return cls.ms_to_timestamp(v) elif f == "marked": diff --git a/pysubs2/time.py b/pysubs2/time.py index 86ac3b8..e2be0c9 100644 --- a/pysubs2/time.py +++ b/pysubs2/time.py @@ -17,8 +17,8 @@ class Times(NamedTuple): ms: int -def make_time(h: IntOrFloat=0, m: IntOrFloat=0, s: IntOrFloat=0, ms: IntOrFloat=0, - frames: Optional[int]=None, fps: Optional[float]=None): +def make_time(h: IntOrFloat = 0, m: IntOrFloat = 0, s: IntOrFloat = 0, ms: IntOrFloat = 0, + frames: Optional[int] = None, fps: Optional[float] = None) -> int: """ Convert time to milliseconds. @@ -43,7 +43,7 @@ def make_time(h: IntOrFloat=0, m: IntOrFloat=0, s: IntOrFloat=0, ms: IntOrFloat= raise ValueError("Both fps and frames must be specified") -def timestamp_to_ms(groups: Sequence[str]): +def timestamp_to_ms(groups: Sequence[str]) -> int: """ Convert groups from :data:`pysubs2.time.TIMESTAMP` or :data:`pysubs2.time.TIMESTAMP_SHORT` match to milliseconds. @@ -55,6 +55,11 @@ def timestamp_to_ms(groups: Sequence[str]): 1000 """ + h: int + m: int + s: int + ms: int + frac: int if len(groups) == 4: h, m, s, frac = map(int, groups) ms = frac * 10**(3 - len(groups[-1])) @@ -70,7 +75,7 @@ def timestamp_to_ms(groups: Sequence[str]): return ms -def times_to_ms(h: IntOrFloat=0, m: IntOrFloat=0, s: IntOrFloat=0, ms: IntOrFloat=0) -> int: +def times_to_ms(h: IntOrFloat = 0, m: IntOrFloat = 0, s: IntOrFloat = 0, ms: IntOrFloat = 0) -> int: """ Convert hours, minutes, seconds to milliseconds. @@ -149,7 +154,7 @@ def ms_to_times(ms: IntOrFloat) -> Times: return Times(h, m, s, ms) -def ms_to_str(ms: IntOrFloat, fractions: bool=False) -> str: +def ms_to_str(ms: IntOrFloat, fractions: bool = False) -> str: """ Prettyprint milliseconds to [-]H:MM:SS[.mmm] diff --git a/pysubs2/tmp.py b/pysubs2/tmp.py index a046bed..74e470b 100644 --- a/pysubs2/tmp.py +++ b/pysubs2/tmp.py @@ -1,11 +1,15 @@ import re import warnings +from typing import Optional, TextIO, Any, TYPE_CHECKING from .formatbase import FormatBase from .ssaevent import SSAEvent from .ssastyle import SSAStyle from .substation import parse_tags from .time import ms_to_times, make_time, TIMESTAMP_SHORT, timestamp_to_ms +if TYPE_CHECKING: + from .ssafile import SSAFile + #: Pattern that matches TMP line TMP_LINE = re.compile(r"(\d{1,2}:\d{2}:\d{2}):(.+)") @@ -29,7 +33,7 @@ def ms_to_timestamp(ms: int) -> str: return f"{h:02d}:{m:02d}:{s:02d}" @classmethod - def guess_format(cls, text): + def guess_format(cls, text: str) -> Optional[str]: """See :meth:`pysubs2.formats.FormatBase.guess_format()`""" if "[Script Info]" in text or "[V4+ Styles]" in text: # disambiguation vs. SSA/ASS @@ -39,12 +43,14 @@ def guess_format(cls, text): if TMP_LINE.match(line) and len(TMP_LINE.findall(line)) == 1: return "tmp" + return None + @classmethod - def from_file(cls, subs, fp, format_, **kwargs): + def from_file(cls, subs: "SSAFile", fp: TextIO, format_: str, **kwargs: Any) -> None: """See :meth:`pysubs2.formats.FormatBase.from_file()`""" events = [] - def prepare_text(text): + def prepare_text(text: str) -> str: text = text.replace("|", r"\N") # convert newlines text = re.sub(r"< *u *>", r"{\\u1}", text) text = re.sub(r"< */? *[a-zA-Z][^>]*>", "", text) # strip other HTML tags @@ -56,7 +62,9 @@ def prepare_text(text): continue start, text = match.groups() - start = timestamp_to_ms(TIMESTAMP_SHORT.match(start).groups()) + match2 = TIMESTAMP_SHORT.match(start) + assert match2 is not None, "TMP_LINE contains TIMESTAMP_SHORT" + start = timestamp_to_ms(match2.groups()) # Unfortunately, end timestamp is not given; try to estimate something reasonable: # start + 500 ms + 67 ms/character (15 chars per second) @@ -72,7 +80,7 @@ def prepare_text(text): subs.events = events @classmethod - def to_file(cls, subs, fp, format_, apply_styles=True, **kwargs): + def to_file(cls, subs: "SSAFile", fp: TextIO, format_: str, apply_styles: bool = True, **kwargs: Any) -> None: """ See :meth:`pysubs2.formats.FormatBase.to_file()` @@ -82,7 +90,7 @@ def to_file(cls, subs, fp, format_, apply_styles=True, **kwargs): apply_styles: If False, do not write any styling. """ - def prepare_text(text, style): + def prepare_text(text: str, style: SSAStyle) -> str: body = [] for fragment, sty in parse_tags(text, style, subs.styles): fragment = fragment.replace(r"\h", " ") diff --git a/pysubs2/webvtt.py b/pysubs2/webvtt.py index 301d673..3493d4d 100644 --- a/pysubs2/webvtt.py +++ b/pysubs2/webvtt.py @@ -1,9 +1,11 @@ import re -from typing import List +from typing import List, Sequence, Optional, TextIO, Any, TYPE_CHECKING -import pysubs2 +from .ssaevent import SSAEvent from .subrip import SubripFormat from .time import make_time +if TYPE_CHECKING: + from .ssafile import SSAFile class WebVTTFormat(SubripFormat): @@ -20,7 +22,7 @@ def ms_to_timestamp(ms: int) -> str: return result.replace(',', '.') @staticmethod - def timestamp_to_ms(groups): + def timestamp_to_ms(groups: Sequence[str]) -> int: _h, _m, _s, _ms = groups if not _h: h = 0 @@ -30,22 +32,24 @@ def timestamp_to_ms(groups): return make_time(h=h, m=m, s=s, ms=ms) @classmethod - def guess_format(cls, text): + def guess_format(cls, text: str) -> Optional[str]: """See :meth:`pysubs2.formats.FormatBase.guess_format()`""" if text.lstrip().startswith("WEBVTT"): return "vtt" + else: + return None @classmethod - def to_file(cls, subs, fp, format_, **kwargs): + def to_file(cls, subs: "SSAFile", fp: TextIO, format_: str, **kwargs: Any) -> None: # type: ignore[override] """ - See :meth:`pysubs2.formats.FormatBase.to_file()` + See :meth:`pysubs2.SubripFormat.to_file()`, additional SRT options are supported by VTT as well """ print("WEBVTT\n", file=fp) return super(WebVTTFormat, cls).to_file( subs=subs, fp=fp, format_=format_, **kwargs) @classmethod - def _get_visible_lines(cls, subs: "pysubs2.SSAFile") -> List["pysubs2.SSAEvent"]: + def _get_visible_lines(cls, subs: "SSAFile") -> List[SSAEvent]: visible_lines = super()._get_visible_lines(subs) visible_lines.sort(key=lambda e: e.start) return visible_lines diff --git a/tests/test_attachment.py b/tests/test_attachment.py index 3d9c917..ce411f3 100644 --- a/tests/test_attachment.py +++ b/tests/test_attachment.py @@ -13,7 +13,7 @@ IMAGE_SUBS_AEGISUB_PATH = op.join(op.dirname(__file__), "data/subtitle_with_attached_images_aegisub.ass") IMAGE_SUBS_PYSUBS_PATH = op.join(op.dirname(__file__), "data/subtitle_with_attached_images_pysubs2_ref.ass") -def test_font_passthrough_from_aegisub(): +def test_font_passthrough_from_aegisub() -> None: subs_aegisub = SSAFile.load(FONT_SUBS_AEGISUB_PATH) subs_pysubs2_ref = SSAFile.load(FONT_SUBS_PYSUBS_PATH) assert subs_aegisub.equals(subs_pysubs2_ref) # sanity check for input @@ -30,12 +30,14 @@ def test_font_passthrough_from_aegisub(): subs_pysubs2 = SSAFile.from_string(subs_pysubs2_text) assert subs_pysubs2_ref.equals(subs_pysubs2) -def test_file_ending_with_font_section(): + +def test_file_ending_with_font_section() -> None: subs = SSAFile.load(FONT_SUBS_NO_EVENTS_PATH) subs_ref = SSAFile.load(FONT_SUBS_PYSUBS_PATH) assert set(subs.fonts_opaque.keys()) == set(subs_ref.fonts_opaque.keys()) -def test_image_passthrough_from_aegisub(): + +def test_image_passthrough_from_aegisub() -> None: subs_aegisub = SSAFile.load(IMAGE_SUBS_AEGISUB_PATH) subs_pysubs2_ref = SSAFile.load(IMAGE_SUBS_PYSUBS_PATH) assert subs_aegisub.equals(subs_pysubs2_ref) # sanity check for input diff --git a/tests/test_cli.py b/tests/test_cli.py index bc39d87..ccfb867 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -23,7 +23,8 @@ {60000}{120000}Subtitle number|two. """ -def test_srt_to_microdvd(): + +def test_srt_to_microdvd() -> None: with tempfile.TemporaryDirectory() as dirpath: inpath = op.join(dirpath, "test.srt") with open(inpath, "w", encoding="utf-8") as fp: @@ -37,12 +38,14 @@ def test_srt_to_microdvd(): out = fp.read() assert out == TEST_MICRODVD_FILE -def test_srt_to_microdvd_subprocess_pipe(): + +def test_srt_to_microdvd_subprocess_pipe() -> None: cmd = ["python", "-m", "pysubs2", "--to", "microdvd", "--fps", "1000"] output = subprocess.check_output(cmd, input=TEST_SRT_FILE, text=True) assert output.strip() == TEST_MICRODVD_FILE.strip() -def test_srt_to_microdvd_multiple_files(): + +def test_srt_to_microdvd_multiple_files() -> None: N = 3 with tempfile.TemporaryDirectory() as dirpath: inpaths = [op.join(dirpath, f"test-{i}.srt") for i in range(N)] @@ -59,7 +62,8 @@ def test_srt_to_microdvd_multiple_files(): out = fp.read() assert out == TEST_MICRODVD_FILE -def test_microdvd_to_srt(): + +def test_microdvd_to_srt() -> None: with tempfile.TemporaryDirectory() as dirpath: inpath = op.join(dirpath, "test.sub") with open(inpath, "w", encoding="utf-8") as fp: @@ -73,6 +77,7 @@ def test_microdvd_to_srt(): out = fp.read() assert out == TEST_SRT_FILE + TEST_SRT_FILE_SHIFTED = """\ 1 01:00:01,500 --> 01:01:01,500 @@ -85,7 +90,8 @@ def test_microdvd_to_srt(): """ -def test_srt_shift(): + +def test_srt_shift() -> None: with tempfile.TemporaryDirectory() as dirpath: inpath = outpath = op.join(dirpath, "test.srt") with open(inpath, "w", encoding="utf-8") as fp: @@ -98,7 +104,8 @@ def test_srt_shift(): out = fp.read() assert out == TEST_SRT_FILE_SHIFTED -def test_srt_shift_back(): + +def test_srt_shift_back() -> None: with tempfile.TemporaryDirectory() as dirpath: inpath = outpath = op.join(dirpath, "test.srt") with open(inpath, "w", encoding="utf-8") as fp: @@ -111,7 +118,8 @@ def test_srt_shift_back(): out = fp.read() assert out == TEST_SRT_FILE -def test_srt_shift_to_output_dir(): + +def test_srt_shift_to_output_dir() -> None: with tempfile.TemporaryDirectory() as indirpath: inpath = op.join(indirpath, "test.srt") with open(inpath, "w", encoding="utf-8") as fp: @@ -132,6 +140,7 @@ def test_srt_shift_to_output_dir(): out = fp.read() assert out == TEST_SRT_FILE + TEST_SUBSTATION_WITH_KARAOKE = r""" [Script Info] ScriptType: v4.00+ @@ -239,7 +248,8 @@ def test_srt_shift_to_output_dir(): Some unsupported tag """ -def test_srt_clean(): + +def test_srt_clean() -> None: # see issue #37 with tempfile.TemporaryDirectory() as dirpath: inpath = op.join(dirpath, "test.ass") @@ -256,7 +266,7 @@ def test_srt_clean(): assert out.strip() == TEST_SUBSTATION_WITH_KARAOKE_SRT_CLEAN_OUTPUT.strip() -def test_srt_clean_styling(): +def test_srt_clean_styling() -> None: # see issue #39 with tempfile.TemporaryDirectory() as dirpath: inpath = op.join(dirpath, "test.ass") @@ -282,7 +292,7 @@ def test_srt_clean_styling(): assert out.strip() == TEST_SUBSTATION_WITH_ITALICS_SRT_CLEAN_OUTPUT.strip() -def test_srt_keep_ssa_tags(): +def test_srt_keep_ssa_tags() -> None: # see issue #48 with tempfile.TemporaryDirectory() as dirpath: path = op.join(dirpath, "test.srt") @@ -309,7 +319,8 @@ def test_srt_keep_ssa_tags(): out = fp.read() assert out.strip() == TEST_SRT_KEEP_SSA_TAGS.strip() -def test_srt_keep_ssa_tags_mixed_with_html(): + +def test_srt_keep_ssa_tags_mixed_with_html() -> None: # see issue #48 with tempfile.TemporaryDirectory() as dirpath: path = op.join(dirpath, "test.srt") @@ -348,7 +359,7 @@ def test_srt_keep_ssa_tags_mixed_with_html(): assert out.strip() == TEST_SRT_KEEP_SSA_TAGS_MIXED_WITH_HTML.strip() -def test_srt_keep_unknown_html_tags(): +def test_srt_keep_unknown_html_tags() -> None: with tempfile.TemporaryDirectory() as dirpath: path = op.join(dirpath, "test.srt") diff --git a/tests/test_common.py b/tests/test_common.py index 908dce9..ad62182 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -1,8 +1,9 @@ from pysubs2 import Color import pytest -def test_color_argument_validation(): - Color(r=0, g=0, b=0) # does not raise + +def test_color_argument_validation() -> None: + Color(r=0, g=0, b=0) # does not raise with pytest.raises(ValueError): Color(r=0, g=0, b=256) diff --git a/tests/test_formats.py b/tests/test_formats.py index 67c59d1..2490224 100644 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -2,25 +2,25 @@ import pysubs2 -def test_identifier_to_class(): +def test_identifier_to_class() -> None: with pytest.raises(pysubs2.UnknownFormatIdentifierError) as exc_info: pysubs2.formats.get_format_class("unknown-format-identifier") assert exc_info.value.format_ == "unknown-format-identifier" -def test_ext_to_identifier(): +def test_ext_to_identifier() -> None: with pytest.raises(pysubs2.UnknownFileExtensionError) as exc_info: pysubs2.formats.get_format_identifier(".xyz") assert exc_info.value.ext == ".xyz" -def test_identifier_to_ext(): +def test_identifier_to_ext() -> None: with pytest.raises(pysubs2.UnknownFormatIdentifierError) as exc_info: pysubs2.formats.get_file_extension("unknown-format-identifier") assert exc_info.value.format_ == "unknown-format-identifier" -def test_format_detection_fail(): +def test_format_detection_fail() -> None: with pytest.raises(pysubs2.FormatAutodetectionError) as exc_info: pysubs2.formats.autodetect_format("") assert not exc_info.value.formats diff --git a/tests/test_json.py b/tests/test_json.py index 11a7444..26a6b1d 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -3,7 +3,8 @@ from pysubs2 import SSAFile, SSAEvent, SSAStyle, Color, FormatAutodetectionError import tempfile -def test_write_read(): + +def test_write_read() -> None: subs = SSAFile() e1 = SSAEvent(text="Hello, world!") e2 = SSAEvent(text="The other subtitle.\\NWith two lines.", style="custom style") @@ -20,7 +21,7 @@ def test_write_read(): assert subs2.equals(subs) -def test_read_unsupported_json_issue_85(): +def test_read_unsupported_json_issue_85() -> None: with tempfile.TemporaryDirectory() as temp_dir: path = op.join(temp_dir, "test.atpj") with open(path, "w") as fp: diff --git a/tests/test_microdvd.py b/tests/test_microdvd.py index b16a7a0..326557b 100644 --- a/tests/test_microdvd.py +++ b/tests/test_microdvd.py @@ -4,7 +4,7 @@ from pysubs2 import SSAFile, SSAEvent, SSAStyle, UnknownFPSError -def test_framerate_inference(): +def test_framerate_inference() -> None: fps = 1000.0 has_fps = dedent("""\ @@ -64,7 +64,7 @@ def test_framerate_inference(): assert subs4[0] == SSAEvent(start=10, end=20, text="Hello!") -def test_extra_whitespace_parsing(): +def test_extra_whitespace_parsing() -> None: f = dedent("""\ { 1 } { 1 } 1000.0 @@ -76,12 +76,14 @@ def test_extra_whitespace_parsing(): subs = SSAFile.from_string(f) assert subs[0] == SSAEvent(start=10, end=20, text="Hello!") -def test_newlines_parsing(): + +def test_newlines_parsing() -> None: f = "{10}{20} So|Many||Newlines ||| " subs = SSAFile.from_string(f, fps=1000) assert subs[0].text == r"So\NMany\N\NNewlines \N\N\N" -def test_tags_parsing(): + +def test_tags_parsing() -> None: f1 = "{10}{20}{Y:i,u}Hello!" subs1 = SSAFile.from_string(f1, fps=1000) assert subs1[0].text == "{\\i1\\u1}Hello!" @@ -103,7 +105,8 @@ def test_tags_parsing(): subs5 = SSAFile.from_string(f5, fps=1000) assert subs5[0].text == r"Hello, {\fs72}world!" -def test_parser_skipping_lines(): + +def test_parser_skipping_lines() -> None: f = dedent("""\ Ook! @@ -121,7 +124,8 @@ def test_parser_skipping_lines(): assert len(subs) == 1 assert subs[0].text == "Hello!" -def test_writer_tags(): + +def test_writer_tags() -> None: subs = SSAFile() subs.styles["italic_style"] = SSAStyle(italic=True) subs.events = [SSAEvent(start=0, end=10, text=r"Plain."), @@ -141,7 +145,8 @@ def test_writer_tags(): assert subs.to_string("microdvd", 1000) == f -def test_writer_uses_original_fps(): + +def test_writer_uses_original_fps() -> None: subs = SSAFile() subs.append(SSAEvent(start=0, end=10, text="Hello!")) subs.fps = 1000 @@ -153,7 +158,8 @@ def test_writer_uses_original_fps(): assert subs.to_string("microdvd") == f -def test_writer_skips_comment_lines(): + +def test_writer_skips_comment_lines() -> None: subs = SSAFile() subs.append(SSAEvent(start=0, end=10, text="Hello!")) subs.append(SSAEvent(start=0, end=10, text="World!")) @@ -166,7 +172,8 @@ def test_writer_skips_comment_lines(): assert subs.to_string("microdvd", fps=1000) == f -def test_writer_handles_whitespace(): + +def test_writer_handles_whitespace() -> None: subs = SSAFile() subs.append(SSAEvent(start=0, end=10, text=r"Hello,\hworld!\NSo many\N\nNewlines.")) @@ -178,7 +185,8 @@ def test_writer_handles_whitespace(): assert subs.to_string("microdvd", fps=1000) == f -def test_writer_strips_tags(): + +def test_writer_strips_tags() -> None: subs = SSAFile() subs.append(SSAEvent(start=0, end=10, text="Let me tell you{a secret}.")) @@ -189,7 +197,8 @@ def test_writer_strips_tags(): assert subs.to_string("microdvd", fps=1000) == f -def test_write_drawing(): + +def test_write_drawing() -> None: subs = SSAFile() subs.append(SSAEvent(start=0, end=10, text=r"{\p1}m 0 0 l 100 0 100 100 0 100{\p0}test")) subs.append(SSAEvent(start=10, end=20, text="Let me tell you.")) diff --git a/tests/test_mpl2.py b/tests/test_mpl2.py index b607ffa..6de4576 100644 --- a/tests/test_mpl2.py +++ b/tests/test_mpl2.py @@ -3,7 +3,7 @@ from pysubs2 import SSAFile, SSAEvent, make_time -def test_simple_parsing(): +def test_simple_parsing() -> None: test_input1 = "[123][456] Line 1" subs1 = SSAFile.from_string(test_input1) assert len(subs1) == 1 @@ -29,7 +29,7 @@ def test_simple_parsing(): assert subs3[2] == SSAEvent(start=make_time(ms=78900), end=make_time(ms=123400), text=r"{\i1}Line 4{\i0}") -def test_simple_writing(): +def test_simple_writing() -> None: subs = SSAFile() subs.append(SSAEvent(start=0, end=1000, text="Hello!")) subs.append(SSAEvent(start=1000, end=2000, text="Hello World!\\NTwo-line subtitle!")) diff --git a/tests/test_parse_tags.py b/tests/test_parse_tags.py index 72fef4a..30dd817 100644 --- a/tests/test_parse_tags.py +++ b/tests/test_parse_tags.py @@ -1,23 +1,27 @@ from pysubs2 import SSAStyle from pysubs2.substation import parse_tags -def test_no_tags(): + +def test_no_tags() -> None: text = "Hello, world!" assert parse_tags(text) == [(text, SSAStyle())] -def test_i_tag(): + +def test_i_tag() -> None: text = "Hello, {\\i1}world{\\i0}!" assert parse_tags(text) == [("Hello, ", SSAStyle()), ("world", SSAStyle(italic=True)), ("!", SSAStyle())] -def test_r_tag(): + +def test_r_tag() -> None: text = "{\\i1}Hello, {\\r}world!" assert parse_tags(text) == [("", SSAStyle()), ("Hello, ", SSAStyle(italic=True)), ("world!", SSAStyle())] -def test_r_named_tag(): + +def test_r_named_tag() -> None: styles = {"other style": SSAStyle(bold=True)} text = "Hello, {\\rother style\\i1}world!" @@ -25,7 +29,8 @@ def test_r_named_tag(): [("Hello, ", SSAStyle()), ("world!", SSAStyle(italic=True, bold=True))] -def test_drawing_tag(): + +def test_drawing_tag() -> None: text = r"{\p1}m 0 0 l 100 0 100 100 0 100{\p0}test" fragments = parse_tags(text) @@ -43,7 +48,8 @@ def test_drawing_tag(): assert drawing_text == "test" assert drawing_style.drawing is False -def test_no_drawing_tag(): + +def test_no_drawing_tag() -> None: text = r"test{\paws}test" fragments = parse_tags(text) diff --git a/tests/test_ssaevent.py b/tests/test_ssaevent.py index 102df85..dfdf179 100644 --- a/tests/test_ssaevent.py +++ b/tests/test_ssaevent.py @@ -2,18 +2,21 @@ from pysubs2 import SSAEvent, make_time -def test_repr_dialogue(): + +def test_repr_dialogue() -> None: ev = SSAEvent(start=make_time(m=1, s=30), end=make_time(m=1, s=35), text="Hello\\Nworld!") ref = r"" assert repr(ev) == ref -def test_repr_comment(): + +def test_repr_comment() -> None: ev = SSAEvent(start=make_time(m=1, s=30), end=make_time(m=1, s=35), text="Hello\\Nworld!") ev.is_comment = True ref = r"" assert repr(ev) == ref -def test_duration(): + +def test_duration() -> None: e = SSAEvent(start=0, end=10) assert e.duration == 10 @@ -29,7 +32,8 @@ def test_duration(): with pytest.raises(ValueError): e.duration = -20 -def test_plaintext(): + +def test_plaintext() -> None: e = SSAEvent(text=r"First\NSecond\NThird\hline{with hidden text}") assert e.plaintext == "First\nSecond\nThird line" @@ -41,7 +45,8 @@ def test_plaintext(): e.plaintext = text assert e.plaintext != text -def test_shift(): + +def test_shift() -> None: e = SSAEvent(start=0, end=10) with pytest.raises(ValueError): @@ -73,7 +78,8 @@ def test_shift(): e2.shift(h=1, m=-60, s=2, ms=-2000) assert e2 == e -def test_fields(): + +def test_fields() -> None: e = SSAEvent() with pytest.warns(DeprecationWarning): assert e.FIELDS == frozenset([ diff --git a/tests/test_ssafile.py b/tests/test_ssafile.py index 5a4ff39..3dc4ae8 100644 --- a/tests/test_ssafile.py +++ b/tests/test_ssafile.py @@ -2,12 +2,14 @@ from pysubs2 import SSAFile, SSAStyle, SSAEvent, make_time -def test_repr_default(): + +def test_repr_default() -> None: subs = SSAFile() ref = "" assert repr(subs) == ref -def test_repr_simple(): + +def test_repr_simple() -> None: subs = SSAFile() subs.append(SSAEvent(start=make_time(m=5), end=make_time(m=6))) subs.append(SSAEvent(start=make_time(m=125), end=make_time(m=126))) @@ -17,7 +19,8 @@ def test_repr_simple(): ref = "" assert repr(subs) == ref -def test_shift(): + +def test_shift() -> None: #TODO: write more tests subs = SSAFile() @@ -30,14 +33,15 @@ def test_shift(): with pytest.raises(ValueError): subs.shift(frames=5, fps=-1) -def test_import_styles(): + +def test_import_styles() -> None: red1 = SSAStyle() red2 = SSAStyle() green = SSAStyle() subs1 = SSAFile() subs2 = SSAFile() - def prepare(): + def prepare() -> None: subs1.styles = {} subs2.styles = {} subs1.styles["green"] = green @@ -55,14 +59,15 @@ def prepare(): assert subs2.styles["red"] is red2 with pytest.raises(TypeError): - subs2.import_styles({}) + subs2.import_styles({}) # type: ignore[arg-type] -def test_rename_style(): + +def test_rename_style() -> None: subs = SSAFile() red = SSAStyle() green = SSAStyle() - def prepare(): + def prepare() -> None: subs.events = [SSAEvent(style="red"), SSAEvent(style="unrelated")] subs.styles = dict(red=red, green=green) @@ -86,7 +91,8 @@ def prepare(): with pytest.raises(KeyError): subs.rename_style("nonexistent-style", "blue") -def test_transform_framerate(): + +def test_transform_framerate() -> None: subs = SSAFile() subs.append(SSAEvent(start=0, end=10)) subs.append(SSAEvent(start=1000, end=1010)) @@ -104,18 +110,20 @@ def test_transform_framerate(): assert subs[0] == SSAEvent(start=0, end=5) assert subs[1] == SSAEvent(start=500, end=505) -def test_insertion_of_wrong_type(): + +def test_insertion_of_wrong_type() -> None: subs = SSAFile() subs.append(SSAEvent()) with pytest.raises(TypeError): - subs.append(42) + subs.append(42) # type: ignore[arg-type] with pytest.raises(TypeError): - subs.insert(42) + subs.insert(42) # type: ignore[call-arg] with pytest.raises(TypeError): - subs[0] = 42 + subs[0] = 42 # type: ignore[call-overload] + -def test_slice_api(): +def test_slice_api() -> None: subs = SSAFile() subs[:] = [ SSAEvent(text="A"), diff --git a/tests/test_ssastyle.py b/tests/test_ssastyle.py index 93d1e73..14d508c 100644 --- a/tests/test_ssastyle.py +++ b/tests/test_ssastyle.py @@ -2,27 +2,32 @@ from pysubs2 import SSAStyle -def test_repr_plain(): + +def test_repr_plain() -> None: ev = SSAStyle(fontname="Calibri", fontsize=36) ref = "" assert repr(ev) == ref -def test_repr_italic(): + +def test_repr_italic() -> None: ev = SSAStyle(fontname="Calibri", fontsize=36, italic=True) ref = "" assert repr(ev) == ref -def test_repr_bold_italic(): + +def test_repr_bold_italic() -> None: ev = SSAStyle(fontname="Calibri", fontsize=36, italic=True, bold=True) ref = "" assert repr(ev) == ref -def test_repr_floatsize(): + +def test_repr_floatsize() -> None: ev = SSAStyle(fontname="Calibri", fontsize=36.499) ref = "" assert repr(ev) == ref -def test_fields(): + +def test_fields() -> None: sty = SSAStyle() with pytest.warns(DeprecationWarning): diff --git a/tests/test_subrip.py b/tests/test_subrip.py index dd424e3..e1bd6cf 100644 --- a/tests/test_subrip.py +++ b/tests/test_subrip.py @@ -9,7 +9,8 @@ from pysubs2 import SSAFile, SSAEvent, make_time from pysubs2.subrip import MAX_REPRESENTABLE_TIME -def test_simple_write(): + +def test_simple_write() -> None: subs = SSAFile() e1 = SSAEvent() @@ -47,7 +48,7 @@ def test_simple_write(): assert text.strip() == ref.strip() -def test_writes_in_given_order(): +def test_writes_in_given_order() -> None: subs = SSAFile() e1 = SSAEvent() @@ -78,7 +79,7 @@ def test_writes_in_given_order(): assert text.strip() == ref.strip() -def test_simple_read(): +def test_simple_read() -> None: text = dedent("""\ 1 00:00:00,000 --> 00:01:00,000 @@ -97,7 +98,8 @@ def test_simple_read(): subs = SSAFile.from_string(text) assert subs.equals(ref) -def test_read_malformed(): + +def test_read_malformed() -> None: """no line number, no empty line, leading whitespace, bad timestamp format""" text = dedent("""\ @@ -115,7 +117,8 @@ def test_read_malformed(): subs = SSAFile.from_string(text) assert subs.equals(ref) -def test_read_position_styling(): + +def test_read_position_styling() -> None: """position is ignored, italic is converted, color is ignored""" text = dedent("""\ @@ -135,7 +138,8 @@ def test_read_position_styling(): subs = SSAFile.from_string(text) assert subs.equals(ref) -def test_read_bad_tags(): + +def test_read_bad_tags() -> None: """missing opening/closing tag, bad nesting, extra whitespace""" text = dedent("""\ @@ -151,7 +155,8 @@ def test_read_bad_tags(): subs = SSAFile.from_string(text) assert subs.equals(ref) -def test_read_tags(): + +def test_read_tags() -> None: text = dedent("""\ 1 00:00:10,500 --> 00:00:13,000 @@ -165,7 +170,8 @@ def test_read_tags(): subs = SSAFile.from_string(text) assert subs.equals(ref) -def test_empty_subtitles(): + +def test_empty_subtitles() -> None: # regression test for issue #11 text = dedent(""" @@ -188,7 +194,8 @@ def test_empty_subtitles(): subs = SSAFile.from_string(text) assert subs.equals(ref) -def test_keep_unknown_html_tags(): + +def test_keep_unknown_html_tags() -> None: # see issue #26 text = dedent("""\ 1 @@ -215,7 +222,8 @@ def test_keep_unknown_html_tags(): assert subs_keep.equals(ref_keep) assert subs_keep.to_string("srt") == ref_keep.to_string("srt") -def test_write_drawing(): + +def test_write_drawing() -> None: # test for 7bde9a6c3a250cf0880a8a9fe31d1b6a69ff21a0 subs = SSAFile() @@ -242,7 +250,8 @@ def test_write_drawing(): text = subs.to_string("srt") assert text.strip() == ref.strip() -def test_keep_ssa_tags(): + +def test_keep_ssa_tags() -> None: # test for issue #48 input_text = dedent("""\ 1 @@ -263,7 +272,8 @@ def test_keep_ssa_tags(): assert input_text.strip() != output_text_do_not_keep_tags.strip() assert input_text.strip() == output_text_keep_tags.strip() -def test_keep_ssa_tags_and_html_tags(): + +def test_keep_ssa_tags_and_html_tags() -> None: # test for issue #48 input_text = dedent("""\ 1 @@ -284,7 +294,8 @@ def test_keep_ssa_tags_and_html_tags(): assert input_text.strip() != output_text_do_not_keep_tags.strip() assert input_text.strip() == output_text_keep_tags.strip() -def test_overflow_timestamp_write(): + +def test_overflow_timestamp_write() -> None: ref = SSAFile() ref.append(SSAEvent(start=make_time(h=1000), end=make_time(h=1001), text="test")) with pytest.warns(RuntimeWarning): diff --git a/tests/test_substation.py b/tests/test_substation.py index 9913b80..06dde6f 100644 --- a/tests/test_substation.py +++ b/tests/test_substation.py @@ -2,7 +2,7 @@ pysubs2.formats.substation tests """ - +import typing from textwrap import dedent from pysubs2 import SSAFile, SSAEvent, SSAStyle, make_time, Color, Alignment from pysubs2.substation import color_to_ass_rgba, color_to_ssa_rgb, rgba_to_color, MAX_REPRESENTABLE_TIME, SubstationFormat @@ -282,7 +282,7 @@ """ -def build_ref(): +def build_ref() -> SSAFile: subs = SSAFile() subs.info["My Custom Info"] = "Some: Test, String." subs.styles["topleft"] = SSAStyle(alignment=Alignment.TOP_LEFT, bold=True) @@ -292,12 +292,14 @@ def build_ref(): subs.append(SSAEvent(start=make_time(m=1), end=make_time(m=2), text="Subtitle number\\Ntwo.")) return subs -def test_simple_write(): + +def test_simple_write() -> None: subs = build_ref() assert subs.to_string("ass").strip() == SIMPLE_ASS_REF.strip() assert subs.to_string("ssa").strip() == SIMPLE_SSA_REF.strip() -def test_simple_read(): + +def test_simple_read() -> None: ref = build_ref() subs1 = SSAFile.from_string(SIMPLE_ASS_REF) subs2 = SSAFile.from_string(SIMPLE_SSA_REF) @@ -305,7 +307,8 @@ def test_simple_read(): assert ref.equals(subs1) assert ref.equals(subs2) -def test_color_parsing(): + +def test_color_parsing() -> None: solid_color = Color(r=1, g=2, b=3) transparent_color = Color(r=1, g=2, b=3, a=4) @@ -316,7 +319,8 @@ def test_color_parsing(): assert rgba_to_color("&HAABBCCDD") == Color(r=0xDD, g=0xCC, b=0xBB, a=0xAA) assert color_to_ass_rgba(Color(r=0xDD, g=0xCC, b=0xBB, a=0xAA)) == "&HAABBCCDD" -def test_aegisub_project_garbage(): + +def test_aegisub_project_garbage() -> None: subs = SSAFile.from_string(AEGISUB_PROJECT_GARBAGE_FILE) garbage_section = dedent(""" [Aegisub Project Garbage] @@ -328,7 +332,9 @@ def test_aegisub_project_garbage(): assert garbage_section in subs.to_string("ass") -def test_ascii_str_fields(): + +@typing.no_type_check +def test_ascii_str_fields() -> None: # see issue #12 STYLE_NAME = b"top-style" @@ -341,7 +347,9 @@ def test_ascii_str_fields(): with pytest.raises(TypeError): subs.to_string("ass") -def test_non_ascii_str_fields(): + +@typing.no_type_check +def test_non_ascii_str_fields() -> None: # see issue #12 STYLE_NAME = "my-style" FONT_NAME = b"NonAsciiString\xff" @@ -355,7 +363,8 @@ def test_non_ascii_str_fields(): with pytest.raises(TypeError): subs.to_string("ass") -def test_negative_timestamp_read(): + +def test_negative_timestamp_read() -> None: ref = build_ref() subs = SSAFile.from_string(NEGATIVE_TIMESTAMP_ASS_REF) @@ -365,7 +374,8 @@ def test_negative_timestamp_read(): # negative times are flushed to zero on output assert ref.to_string("ass") == subs.to_string("ass") -def test_overflow_timestamp_write(): + +def test_overflow_timestamp_write() -> None: ref = build_ref() ref[0].end = make_time(h=1000) with pytest.warns(RuntimeWarning): @@ -373,7 +383,8 @@ def test_overflow_timestamp_write(): subs = SSAFile.from_string(text) assert subs[0].end == MAX_REPRESENTABLE_TIME -def test_centisecond_rounding(): + +def test_centisecond_rounding() -> None: ref = SSAFile() ref.append(SSAEvent(start=make_time(h=1, m=1, ms=4), end=make_time(h=1, m=1, ms=5))) text = ref.to_string("ass") @@ -381,7 +392,8 @@ def test_centisecond_rounding(): assert subs[0].start == make_time(h=1, m=1, ms=0) assert subs[0].end == make_time(h=1, m=1, ms=10) -def test_no_space_after_colon_in_metadata_section(): + +def test_no_space_after_colon_in_metadata_section() -> None: # see issue #14 ref = SSAFile.from_string(AEGISUB_PROJECT_GARBAGE_FILE) subs = SSAFile.from_string(AEGISUB_PROJECT_GARBAGE_FILE_WITHOUT_SPACE_AFTER_COLON) @@ -389,7 +401,8 @@ def test_no_space_after_colon_in_metadata_section(): assert ref.equals(subs) assert ref.aegisub_project == subs.aegisub_project -def test_hex_color_in_ssa(): + +def test_hex_color_in_ssa() -> None: # see issue #32 subs = SSAFile.from_string(HEX_COLOR_IN_SSA) style = subs.styles["Default"] @@ -397,14 +410,14 @@ def test_hex_color_in_ssa(): assert style.secondarycolor == Color(r=0xff, g=0xff, b=0x00) -def test_ass_with_malformed_style(): +def test_ass_with_malformed_style() -> None: # see issue #45 subs = SSAFile.from_string(ASS_WITH_MALFORMED_STYLE) assert subs[0].text == "Hello" assert subs.styles["Default"].fontname == "Arial" -def test_ass_with_missing_fractions_in_timestamp(): +def test_ass_with_missing_fractions_in_timestamp() -> None: # see issue #50 subs = SSAFile.from_string(ASS_WITHOUT_FRACTIONS_OF_SECOND_REF) @@ -421,7 +434,7 @@ def test_ass_with_missing_fractions_in_timestamp(): assert subs[2].end == make_time(0, 1, 23, 0) -def test_ass_with_short_minutes_seconds_in_timestamp(): +def test_ass_with_short_minutes_seconds_in_timestamp() -> None: # see pull request #54 subs = SSAFile.from_string(ASS_WITH_SHORT_MINUTES_SECONDS_REF) @@ -433,8 +446,9 @@ def test_ass_with_short_minutes_seconds_in_timestamp(): assert subs[1].end == make_time(0, 0, 7, 880) +@typing.no_type_check @pytest.mark.filterwarnings("ignore:.*should be an Alignment instance.*:DeprecationWarning") -def test_alignment_given_as_integer(): +def test_alignment_given_as_integer() -> None: subs = SSAFile() subs.info["My Custom Info"] = "Some: Test, String." subs.styles["topleft"] = SSAStyle(alignment=7, bold=True) @@ -447,13 +461,13 @@ def test_alignment_given_as_integer(): assert subs.to_string("ssa").strip() == SIMPLE_SSA_REF.strip() -def test_reading_invalid_alignment_raises_warning(): +def test_reading_invalid_alignment_raises_warning() -> None: with pytest.warns(RuntimeWarning): subs = SSAFile.from_string(ASS_WITH_MALFORMED_STYLE_INVALID_ALIGNMENT) assert subs.styles["Default"].alignment == Alignment.BOTTOM_CENTER -def test_ass_ms_to_timestamp(): +def test_ass_ms_to_timestamp() -> None: # see issue #76 assert SubstationFormat.ms_to_timestamp(4659990) == "1:17:39.99" @@ -468,7 +482,7 @@ def test_ass_ms_to_timestamp(): assert SubstationFormat.ms_to_timestamp(4659999) == "1:17:40.00" -def test_bad_style_format_line_issue_89(): +def test_bad_style_format_line_issue_89() -> None: subs = SSAFile.from_string(ASS_STYLES_FORMAT_ISSUE_89) assert subs.styles["Default"].bold assert subs.styles["Default"].italic @@ -476,7 +490,7 @@ def test_bad_style_format_line_issue_89(): assert not subs.styles["Default"].strikeout -def test_empty_layer_issue_87(): +def test_empty_layer_issue_87() -> None: with pytest.warns(RuntimeWarning, match="Failed to parse layer"): subs = SSAFile.from_string(ASS_EMPTY_LAYERS_ISSUE_87) assert subs[0].layer == 0 diff --git a/tests/test_time.py b/tests/test_time.py index 044745f..8a8eadf 100644 --- a/tests/test_time.py +++ b/tests/test_time.py @@ -2,8 +2,8 @@ pysubs2.time tests """ - -from fractions import Fraction +import typing +from fractions import Fraction import pytest from pysubs2.time import TIMESTAMP, TIMESTAMP_SHORT, timestamp_to_ms, times_to_ms, ms_to_times, Times, frames_to_ms, \ @@ -11,21 +11,21 @@ # helper functions -def cs2ms(cs): +def cs2ms(cs: int) -> int: return 10 * cs -def s2ms(s): +def s2ms(s: int) -> int: return 1000 * s -def m2ms(m): +def m2ms(m: int) -> int: return 60 * 1000 * m -def h2ms(h): +def h2ms(h: int) -> int: return 60 * 60 * 1000 * h - -def test_timestamp(): +@typing.no_type_check +def test_timestamp() -> None: # proper SSA assert TIMESTAMP.match("1:23:45.67").groups() == ("1", "23", "45", "67") @@ -56,7 +56,8 @@ def test_timestamp(): assert TIMESTAMP.match(":12:45.67") is None -def test_timestamp_short(): +@typing.no_type_check +def test_timestamp_short() -> None: # proper TMP assert TIMESTAMP_SHORT.match("01:23:45").groups() == ("01", "23", "45") @@ -73,7 +74,8 @@ def test_timestamp_short(): assert TIMESTAMP_SHORT.match(":12:45") is None -def test_timestamp_to_ms(): +@typing.no_type_check +def test_timestamp_to_ms() -> None: # proper SSA assert timestamp_to_ms(TIMESTAMP.match("1:23:45.67").groups()) == \ h2ms(1) + m2ms(23) + s2ms(45) + cs2ms(67) @@ -94,7 +96,8 @@ def test_timestamp_to_ms(): h2ms(10) + m2ms(23) + s2ms(45) -def test_times_to_ms(): +@typing.no_type_check +def test_times_to_ms() -> None: # basic tests assert times_to_ms() == 0 assert times_to_ms(h=5) == h2ms(5) @@ -119,7 +122,8 @@ def test_times_to_ms(): assert times_to_ms(*ms_to_times(123456)) == 123456 -def test_ms_to_times(): +@typing.no_type_check +def test_ms_to_times() -> None: # basic tests assert ms_to_times(0) == (0, 0, 0, 0) assert isinstance(ms_to_times(0), Times) @@ -147,7 +151,8 @@ def test_ms_to_times(): assert ms_to_times(times_to_ms(h=1,m=2,s=3,ms=4)) == (1, 2, 3, 4) -def test_frames_to_ms(): +@typing.no_type_check +def test_frames_to_ms() -> None: # basic tests assert frames_to_ms(0, 25) == 0 assert isinstance(frames_to_ms(0, 25), int) @@ -164,7 +169,9 @@ def test_frames_to_ms(): with pytest.raises(TypeError): frames_to_ms(frames=1, fps="pal") # keyword aliases from PySubs 0.1 are no longer supported -def test_ms_to_frames(): + +@typing.no_type_check +def test_ms_to_frames() -> None: # basic tests assert ms_to_frames(0, 25) == 0 assert isinstance(ms_to_frames(0, 25), int) @@ -181,7 +188,8 @@ def test_ms_to_frames(): with pytest.raises(TypeError): ms_to_frames(1, fps="pal") # keyword aliases from PySubs 0.1 are no longer supported -def test_ms_to_str(): + +def test_ms_to_str() -> None: assert ms_to_str(0) == "0:00:00" assert ms_to_str(0, fractions=True) == "0:00:00.000" assert ms_to_str(1) == "0:00:00" diff --git a/tests/test_tmp.py b/tests/test_tmp.py index 4df68a8..c687ab5 100644 --- a/tests/test_tmp.py +++ b/tests/test_tmp.py @@ -9,7 +9,8 @@ from pysubs2 import SSAFile, SSAEvent, make_time from pysubs2.tmp import MAX_REPRESENTABLE_TIME -def test_simple_write(): + +def test_simple_write() -> None: subs = SSAFile() e1 = SSAEvent() @@ -41,7 +42,7 @@ def test_simple_write(): assert text.strip() == ref.strip() -def test_simple_read(): +def test_simple_read() -> None: text = dedent("""\ 00:00:00:ten--chars 00:01:00:ten--chars-ten-chars @@ -54,7 +55,8 @@ def test_simple_read(): subs = SSAFile.from_string(text) assert subs.equals(ref) -def test_overlapping_read(): + +def test_overlapping_read() -> None: # see issue #35 text = dedent("""\ 00:00:12:I ... this is some long text ... ... this is some long text ... @@ -68,7 +70,8 @@ def test_overlapping_read(): assert subs[1].end == subs[2].start == make_time(s=18) assert subs[2].end == subs[3].start == make_time(s=22) -def test_styled_read(): + +def test_styled_read() -> None: text = dedent("""\ 00:00:00:ten--chars--underline 00:01:00:ten--chars--some--tags @@ -78,7 +81,8 @@ def test_styled_read(): assert subs[0].text == r"ten--chars--{\u1}underline" assert subs[1].text == "ten--chars--some--tags" -def test_write_drawing(): + +def test_write_drawing() -> None: subs = SSAFile() e1 = SSAEvent() @@ -108,7 +112,8 @@ def test_write_drawing(): text = subs.to_string("tmp") assert text.strip() == ref.strip() -def test_overflow_timestamp_write(): + +def test_overflow_timestamp_write() -> None: ref = SSAFile() ref.append(SSAEvent(start=make_time(h=1000), end=make_time(h=1001), text="test")) with pytest.warns(RuntimeWarning): diff --git a/tests/test_vtt.py b/tests/test_vtt.py index ba7c9ae..c2c0c8f 100644 --- a/tests/test_vtt.py +++ b/tests/test_vtt.py @@ -6,7 +6,8 @@ from textwrap import dedent from pysubs2 import SSAFile, SSAEvent, make_time -def test_simple_write(): + +def test_simple_write() -> None: subs = SSAFile() e1 = SSAEvent() @@ -45,7 +46,8 @@ def test_simple_write(): text = subs.to_string("vtt") assert text.strip() == ref.strip() -def test_writes_in_time_order(): + +def test_writes_in_time_order() -> None: subs = SSAFile() e1 = SSAEvent() @@ -77,7 +79,8 @@ def test_writes_in_time_order(): text = subs.to_string("vtt") assert text.strip() == ref.strip() -def test_simple_read(): + +def test_simple_read() -> None: text = dedent("""\ WEBVTT @@ -98,7 +101,8 @@ def test_simple_read(): subs = SSAFile.from_string(text) assert subs.equals(ref) -def test_read_complex(): + +def test_read_complex() -> None: # regression test for #30 text = dedent("""\ WEBVTT diff --git a/tests/test_whisper.py b/tests/test_whisper.py index 7c35076..ccdf2c0 100644 --- a/tests/test_whisper.py +++ b/tests/test_whisper.py @@ -15,7 +15,7 @@ 'no_speech_prob': 0.0026147987227886915}], 'language': 'en'} -def test_read_whisper_transcript_dict(): +def test_read_whisper_transcript_dict() -> None: subs = pysubs2.load_from_whisper(TRANSCRIBE_RESULT) e1, e2 = subs @@ -27,8 +27,8 @@ def test_read_whisper_transcript_dict(): assert e2.text == "ask what you can do for your country." -def test_read_whisper_segments_list(): - subs = pysubs2.load_from_whisper(TRANSCRIBE_RESULT["segments"]) +def test_read_whisper_segments_list() -> None: + subs = pysubs2.load_from_whisper(TRANSCRIBE_RESULT["segments"]) # type: ignore[arg-type] e1, e2 = subs assert e1.start == 0