From 155d9254f8380fb02e01ace400fb56a800580ff1 Mon Sep 17 00:00:00 2001 From: Igor Kozyrenko Date: Sat, 9 Sep 2023 14:12:23 -0700 Subject: [PATCH 1/3] fix: type check in equality operator, no comparison should raise now --- srt.py | 2 +- tests/test_srt.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/srt.py b/srt.py index 9d0b586..905e334 100755 --- a/srt.py +++ b/srt.py @@ -126,7 +126,7 @@ def __hash__(self): return hash(frozenset(vars(self).items())) def __eq__(self, other): - return vars(self) == vars(other) + return isinstance(other, Subtitle) and vars(self) == vars(other) def __lt__(self, other): return (self.start, self.end, self.index) < ( diff --git a/tests/test_srt.py b/tests/test_srt.py index 05608e0..5e403c4 100644 --- a/tests/test_srt.py +++ b/tests/test_srt.py @@ -241,6 +241,12 @@ def test_subtitle_inequality(sub_1): assert sub_1 != sub_2 +@given(subtitles()) +def test_subtitle_inequality_to_non_matching_type(sub_1): + assert sub_1 != None + assert sub_1 != 1 + + @given(subtitles()) def test_subtitle_from_scratch_equality(subtitle): srt_block = subtitle.to_srt() From ff96458d16e8e9f28315687fbed2bb23b013c6b0 Mon Sep 17 00:00:00 2001 From: Chris Down Date: Thu, 2 Nov 2023 00:37:24 +0000 Subject: [PATCH 2/3] ci: Remove Python 2.7 This is no longer supported or operational. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 281b0f6..e785aae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,7 @@ jobs: matrix: # Pin to 20.04 for 3.6: https://github.com/actions/setup-python/issues/544 os: [ubuntu-20.04, macos-latest, windows-latest] - python-version: ['2.7', '3.5', '3.6', '3.7', '3.8', '3.9', '3.10', '3.11'] + python-version: ['3.5', '3.6', '3.7', '3.8', '3.9', '3.10', '3.11'] runs-on: ${{ matrix.os }} steps: From f4022896fccaaa6731911e38bac9fe7877592175 Mon Sep 17 00:00:00 2001 From: Brian Willows Date: Sun, 27 Sep 2026 16:32:41 +0100 Subject: [PATCH 3/3] fix: parse whitespace-only input in linear time SRT_REGEX starts with \s* (added for #50), so finditer retries every position of a long whitespace run and each retry rescans the rest of the run: 32000 spaces or newlines took about 22 seconds to parse. A match starting at p with whitespace at p - 1 implies a match at p - 1, so the leftmost match is either at the expected position or at one not preceded by whitespace. _iter_srt_matches tries the expected position first and otherwise searches with a (? Function taking a Subtitle, skip if True @@ -370,7 +374,7 @@ def parse(srt, ignore_errors=False): if isinstance(srt, FILE_TYPES): srt = srt.read() - for match in SRT_REGEX.finditer(srt): + for match in _iter_srt_matches(srt): actual_start = match.start() _check_contiguity(srt, expected_start, actual_start, ignore_errors) raw_index, raw_start, raw_end, proprietary, content = match.groups() @@ -406,6 +410,29 @@ def parse(srt, ignore_errors=False): _check_contiguity(srt, expected_start, len(srt), ignore_errors) +def _iter_srt_matches(srt): + """ + Yield the same matches as ``SRT_REGEX.finditer(srt)``, in linear rather + than quadratic time on long whitespace runs that contain no subtitle. + + ``SRT_REGEX`` starts with ``\\s*``, so if a match starts at position ``p`` + and ``srt[p - 1]`` is whitespace, a match also starts at ``p - 1``. The + leftmost match at or after ``pos`` therefore either starts at ``pos`` or + at a position not preceded by whitespace. ``finditer`` retries every + position inside a whitespace run, and each retry rescans the rest of the + run; ``_SRT_REGEX_NOT_AFTER_WS`` skips those positions in constant time. + """ + pos = 0 + while pos <= len(srt): + match = SRT_REGEX.match(srt, pos) + if match is None: + match = _SRT_REGEX_NOT_AFTER_WS.search(srt, pos + 1) + if match is None: + return + yield match + pos = match.end() + + def _check_contiguity(srt, expected_start, actual_start, warn_only): """ If ``warn_only`` is False, raise :py:class:`SRTParseError` with diagnostic diff --git a/tests/test_srt.py b/tests/test_srt.py index 5e403c4..33b7f3a 100644 --- a/tests/test_srt.py +++ b/tests/test_srt.py @@ -8,6 +8,7 @@ import os import re import string +import time from io import StringIO import pytest @@ -771,3 +772,24 @@ def test_parse_file_with_missing_index(input_subs): # cf. issue #51 # ...and sort the same. assert srt.compose(subs_no_index) == srt.compose(subs_zero_index) + + +@given(st.text(alphabet=" \t\r\n0123456789:,->x", max_size=60)) +def test_iter_srt_matches_same_as_finditer(text): + # _iter_srt_matches is a faster finditer; it must find exactly the same + # matches, including on garbage and whitespace between subtitles. + subs_text = "1\n00:00:01,000 --> 00:00:02,000\nhello\n\n" + for srt_data in (text, subs_text + text, text + subs_text): + expected = [(m.span(), m.groups()) for m in srt.SRT_REGEX.finditer(srt_data)] + actual = [(m.span(), m.groups()) for m in srt._iter_srt_matches(srt_data)] + assert actual == expected + + +def test_parse_long_whitespace_is_not_quadratic(): + # Each finditer retry inside a whitespace run used to rescan the rest of + # the run: about 20 seconds for 32000 spaces. + for srt_data in (" " * 32000, "\n" * 32000, "x" + " " * 32000): + start = time.perf_counter() + subs = list(srt.parse(srt_data, ignore_errors=True)) + assert time.perf_counter() - start < 1 + assert subs == []