From fd61ab6918c91201aa92380f3ee3356968eea901 Mon Sep 17 00:00:00 2001 From: "eduard.panasicov" Date: Mon, 14 Sep 2026 14:35:16 +0300 Subject: [PATCH 01/12] OCTO-11566 Create code checks script(pre-commit + ruff) --- .pre-commit-config.yaml | 27 +++++++------ Dockerfile_precommit | 15 +++++++ ruff.toml | 56 ++++++++++++++++++++++++++ scripts/code_checks/run_code_checks.sh | 20 +++++++++ 4 files changed, 106 insertions(+), 12 deletions(-) create mode 100644 Dockerfile_precommit create mode 100644 ruff.toml create mode 100755 scripts/code_checks/run_code_checks.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index afa4a1d3..343acbc6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,17 +1,20 @@ +default_stages: [pre-commit, pre-push] repos: -- repo: git://github.com/pre-commit/pre-commit-hooks - rev: v4.0.1 +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 hooks: - - id: end-of-file-fixer + - id: check-toml + - id: check-xml + - id: check-yaml - id: trailing-whitespace - - id: debug-statements + - id: requirements-txt-fixer + - id: sort-simple-yaml + - id: end-of-file-fixer + - id: check-ast -- repo: git://github.com/PyCQA/flake8 - rev: 3.9.2 +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.16.5 hooks: - - id: flake8 - args: [ - '--exclude=tests/fixtures*', - '--ignore=W503,C901', - '--max-line-length=80', - ] + - id: ruff + args: [--fix] + - id: ruff-format diff --git a/Dockerfile_precommit b/Dockerfile_precommit new file mode 100644 index 00000000..ce8c963f --- /dev/null +++ b/Dockerfile_precommit @@ -0,0 +1,15 @@ +# minimum supported version +FROM python:3.10-slim-bookworm + +WORKDIR /pycaption + +ADD ./.pre-commit-config.yaml /pycaption/ +ADD ./ruff.toml /pycaption/ + +RUN apt-get -y update && \ + apt-get -y install git && \ + git init && \ + pip install pre-commit==4.6.2 && \ + pre-commit install --install-hooks + +RUN git config --global --add safe.directory /pycaption diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 00000000..a82ad071 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,56 @@ +# Ruff configuration — replaces flake8, isort, black, pyupgrade, autoflake, bandit + +# Match black's default line length +line-length = 88 + +[lint] +# Rule sets: +# E, W = pycodestyle (flake8) +# F = pyflakes (flake8 + autoflake) +# C90 = mccabe complexity (flake8 max-complexity) +# I = isort +# UP = pyupgrade +# S = bandit (flake8-bandit) +select = ["E", "W", "F", "C90", "I", "UP", "S", "T10"] + +# Match .flake8 ignore list + bandit LOW/MEDIUM severity rules +# (original bandit config used --severity-level high) +ignore = [ + "S101", # assert_used (LOW) + "S108", # hardcoded_tmp_directory (MEDIUM) + "S110", # try_except_pass (LOW) + "S112", # try_except_continue (LOW) + "S113", # request_without_timeout (MEDIUM) + "S305", # insecure cipher mode (MEDIUM) + "S307", # eval (MEDIUM) + "S308", # mark_safe (MEDIUM) + "S310", # urllib_urlopen (MEDIUM) + "S311", # random (LOW) + "S314", # xml_bad_etree (MEDIUM) + "S603", # subprocess without shell=True (LOW) + "S607", # start_process_with_partial_path (LOW) + "S608", # hardcoded_sql_expressions (MEDIUM) +] + +# Enable auto-fix (replaces autoflake --remove-all-unused-imports + pyupgrade --in-place) +fixable = ["ALL"] + +[lint.per-file-ignores] +# Match .flake8 per-file-ignores: allow unused and star imports in __init__.py +"__init__.py" = ["F401", "F403"] +# Match bandit exclude: no security checks on tests +"**/tests/**" = ["S"] + +[lint.mccabe] +# Match .flake8 max-complexity +max-complexity = 10 + +[lint.isort] +# Match isort settings: --profile black +known-first-party = ["pycaption"] +lines-between-types = 1 +lines-after-imports = 2 + +[format] +# Match black --skip-string-normalization +quote-style = "preserve" diff --git a/scripts/code_checks/run_code_checks.sh b/scripts/code_checks/run_code_checks.sh new file mode 100755 index 00000000..146aa70d --- /dev/null +++ b/scripts/code_checks/run_code_checks.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +# script used for manually running all pre-commit hooks +# run this script locally and make sure no hook fails before pushing changes +# test and deploy plans run this script and the whole plan will fail if any hook fails + +SCRIPT_PATH=$(readlink -f $0) # get absolute path of this script + +# get absolute path to the parent directory of this script (pycaption/scripts/code_checks) +CODE_CHECKS_DIR=$(dirname $SCRIPT_PATH) + +PYCAPTION_DIR=$(dirname $(dirname $CODE_CHECKS_DIR)) # get pycaption absolute path +cd $PYCAPTION_DIR # cd to pycaption directory + +# builds pre-commit environment +docker build . --file="./Dockerfile_precommit" --tag="pycaption_code_checks:latest" + +# run pre-commit on all files with all the other arguments passed along to this script +docker run --rm -v $PWD:/pycaption pycaption_code_checks:latest bash -c \ + "pre-commit run --all-files $*" From c76ecb96f81e8c32e6180042449923b8ab4a8df7 Mon Sep 17 00:00:00 2001 From: "eduard.panasicov" Date: Mon, 14 Sep 2026 15:04:40 +0300 Subject: [PATCH 02/12] OCTO-11566 Add exclusions in pre-commit and ruff --- .pre-commit-config.yaml | 8 ++++++++ ruff.toml | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 343acbc6..c83fba75 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,12 @@ default_stages: [pre-commit, pre-push] + +exclude: | + (?x)( + ^tests/fixtures/| + ^ai_artifacts/| + ^.claude/ + ) + repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 diff --git a/ruff.toml b/ruff.toml index a82ad071..3b2d23a9 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,5 +1,12 @@ # Ruff configuration — replaces flake8, isort, black, pyupgrade, autoflake, bandit +# Global exclusions (match current .pre-commit-config.yaml exclude patterns) +exclude = [ + "tests/fixtures/", + "ai_artifacts/", + ".claude/", +] + # Match black's default line length line-length = 88 From cde0f36e67b705245b5f6b4fb5db36231b5887ea Mon Sep 17 00:00:00 2001 From: "eduard.panasicov" Date: Mon, 14 Sep 2026 15:05:01 +0300 Subject: [PATCH 03/12] OCTO-11566 Add noqa unused imports in tests --- tests/conftest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 48781201..13af5f21 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -51,7 +51,7 @@ sample_dfxp_without_region_and_style, ) from tests.fixtures.microdvd import missing_fps_sample_microdvd # noqa: F401 -from tests.fixtures.microdvd import ( +from tests.fixtures.microdvd import ( # noqa: F401 sample_microdvd, sample_microdvd_2, sample_microdvd_empty, @@ -59,7 +59,7 @@ sample_microdvd_invalid_format, ) from tests.fixtures.sami import sample_sami # noqa: F401 -from tests.fixtures.sami import ( +from tests.fixtures.sami import ( # noqa: F401 sample_sami_double_br, sample_sami_empty, sample_sami_empty_cue_output, @@ -142,7 +142,7 @@ scc_that_generates_webvtt_with_proper_newlines, ) from tests.fixtures.srt import sample_srt_ascii # noqa: F401 -from tests.fixtures.srt import ( +from tests.fixtures.srt import ( # noqa: F401 sample_srt, sample_srt_blank_lines, sample_srt_empty, From d150b98d9f953b1cb8685bd7cdbc503b6c14f96e Mon Sep 17 00:00:00 2001 From: "eduard.panasicov" Date: Mon, 14 Sep 2026 15:08:09 +0300 Subject: [PATCH 04/12] OCTO-11566 Ruff auto fixes --- .github/workflows/unit_tests.yml | 2 +- .readthedocs.yaml | 2 +- docker-compose.yml | 2 +- docs/conf.py | 4 +- docs/requirements.txt | 2 +- pycaption/__init__.py | 1 + pycaption/base.py | 5 +- pycaption/dfxp/constants.py | 1 + pycaption/dfxp/extras.py | 1 + pycaption/dfxp/reader.py | 10 +-- pycaption/dfxp/writer.py | 2 + pycaption/geometry.py | 3 + pycaption/microdvd.py | 1 + pycaption/sami/constants.py | 1 + pycaption/sami/reader.py | 1 + pycaption/sami/writer.py | 1 + pycaption/scc/__init__.py | 1 + pycaption/scc/constants.py | 2 + pycaption/scc/reader.py | 2 + pycaption/scc/specialized_collections.py | 1 + pycaption/scc/writer.py | 8 +- .../caption_preview/caption_preview.py | 8 +- pycaption/srt.py | 7 +- pycaption/webvtt/__init__.py | 1 + pycaption/webvtt/constants.py | 1 + pycaption/webvtt/writer.py | 1 + run_tests.sh | 2 - setup.py | 3 +- test_requirements.txt | 6 +- tests/conftest.py | 6 +- tests/mixins.py | 1 + tests/test_bytes_input.py | 2 - tests/test_dfxp_conversion.py | 7 +- tests/test_double_encoding.py | 4 +- tests/test_sami.py | 10 +-- tests/test_sami_conversion.py | 11 +-- tests/test_scc.py | 61 ++++++++------- tests/test_scc_conversion.py | 7 +- tests/test_scc_translator.py | 4 +- tests/test_scc_writer.py | 78 +++++++------------ tests/test_webvtt.py | 44 +++-------- tests/test_webvtt_conversion.py | 54 +++++-------- 42 files changed, 168 insertions(+), 203 deletions(-) diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index d1de2665..a33f7497 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -28,7 +28,7 @@ jobs: - name: Run Test id: tests run: | - ./run_tests.sh test_${{ matrix.python-version }} + ./run_tests.sh test_${{ matrix.python-version }} continue-on-error: true - name: Archive production artifacts diff --git a/.readthedocs.yaml b/.readthedocs.yaml index f7e3b63f..de6be133 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -12,4 +12,4 @@ sphinx: # Explicitly set the version of Python and its requirements python: install: - - requirements: docs/requirements.txt \ No newline at end of file + - requirements: docs/requirements.txt diff --git a/docker-compose.yml b/docker-compose.yml index 4c064836..43678306 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,4 +35,4 @@ services: pytest -vvvv --color=yes --junit-xml=junit.xml --cov=pycaption --cov-report xml:coverage.xml; " volumes: - - .:/pycaption \ No newline at end of file + - .:/pycaption diff --git a/docs/conf.py b/docs/conf.py index b6f2b5ec..effc8c7a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,6 +13,7 @@ import sphinx_rtd_theme + # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. @@ -45,8 +46,7 @@ # General information about the project. project = "pycaption" -copyright = "2012-2026, PBS.org " \ - "(available under the Apache License, Version 2.0)" +copyright = "2012-2026, PBS.org (available under the Apache License, Version 2.0)" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/docs/requirements.txt b/docs/requirements.txt index d8d1ed24..9aa6704d 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,3 @@ +readthedocs-sphinx-search==0.3.2 sphinx==7.2.6 sphinx_rtd_theme==1.3.0 -readthedocs-sphinx-search==0.3.2 \ No newline at end of file diff --git a/pycaption/__init__.py b/pycaption/__init__.py index 47ad89b4..e54f219b 100644 --- a/pycaption/__init__.py +++ b/pycaption/__init__.py @@ -21,6 +21,7 @@ from .transcript import TranscriptWriter from .webvtt import WebVTTReader, WebVTTWriter + __all__ = [ "CaptionConverter", "DFXPReader", diff --git a/pycaption/base.py b/pycaption/base.py index 5cafdb21..53ea39d5 100644 --- a/pycaption/base.py +++ b/pycaption/base.py @@ -7,11 +7,13 @@ import logging import os + from datetime import timedelta from numbers import Number from .exceptions import CaptionReadError, CaptionReadTimingError, InvalidInputError + logger = logging.getLogger(__name__) # `und` a special identifier for an undetermined language according to ISO 639-2 @@ -40,8 +42,7 @@ def read(self, content, caption_reader): """ if not hasattr(caption_reader, "read"): raise InvalidInputError( - "The caption_reader must be a BaseReader instance " - "with a read() method." + "The caption_reader must be a BaseReader instance with a read() method." ) self.captions = caption_reader.read(content) return self diff --git a/pycaption/dfxp/constants.py b/pycaption/dfxp/constants.py index 00f9fceb..ee3ed2b6 100644 --- a/pycaption/dfxp/constants.py +++ b/pycaption/dfxp/constants.py @@ -8,6 +8,7 @@ from ..geometry import Alignment, HorizontalAlignmentEnum, Layout, VerticalAlignmentEnum + DFXP_BASE_MARKUP = """ diff --git a/pycaption/dfxp/extras.py b/pycaption/dfxp/extras.py index 39702f51..37ffc7e6 100644 --- a/pycaption/dfxp/extras.py +++ b/pycaption/dfxp/extras.py @@ -13,6 +13,7 @@ from .constants import DFXP_DEFAULT_REGION from .writer import DFXPWriter + LEGACY_DFXP_BASE_MARKUP = """ diff --git a/pycaption/dfxp/reader.py b/pycaption/dfxp/reader.py index 95d7dec8..cb52718e 100644 --- a/pycaption/dfxp/reader.py +++ b/pycaption/dfxp/reader.py @@ -47,6 +47,7 @@ VERTICAL_ALIGNMENT_TO_DFXP, ) + _LEADING_WHITESPACE_RE = re.compile("^(?:[\n\r]+\\s*)?(.+)") _DFXP_WRITING_MODE_MAP = { @@ -147,7 +148,8 @@ def read(self, content): style_dict[id_] = self._convert_style(style) caption_set = CaptionSet( - caption_dict, styles=style_dict, + caption_dict, + styles=style_dict, visual_alignment_default=HorizontalAlignmentEnum.START, ) @@ -179,8 +181,7 @@ def _resolve_explicit_tickrate(self, tt_attrs): tickrate = float(tt_attrs["ttp:tickrate"]) except ValueError: raise CaptionReadSyntaxError( - f"ttp:tickRate must be a number, " - f"got '{tt_attrs['ttp:tickrate']}'" + f"ttp:tickRate must be a number, got '{tt_attrs['ttp:tickrate']}'" ) if tickrate <= 0: raise CaptionReadSyntaxError( @@ -203,8 +204,7 @@ def _resolve_default_tickrate(self, tt_attrs, framerate_str): framerate_int = int(framerate_str) except ValueError: raise CaptionReadSyntaxError( - f"ttp:frameRate must be a positive integer, " - f"got '{framerate_str}'" + f"ttp:frameRate must be a positive integer, got '{framerate_str}'" ) self.tickrate = float(framerate_int * sub_framerate) diff --git a/pycaption/dfxp/writer.py b/pycaption/dfxp/writer.py index c1e9aeea..395d020f 100644 --- a/pycaption/dfxp/writer.py +++ b/pycaption/dfxp/writer.py @@ -5,6 +5,7 @@ """ import re + from copy import deepcopy from xml.sax.saxutils import escape @@ -26,6 +27,7 @@ _create_external_alignment, ) + _WRITING_DIRECTION_TO_DFXP = { WritingDirectionEnum.VERTICAL_RL: "tbrl", WritingDirectionEnum.VERTICAL_LR: "tblr", diff --git a/pycaption/geometry.py b/pycaption/geometry.py index fc9b00d4..2c838dd9 100644 --- a/pycaption/geometry.py +++ b/pycaption/geometry.py @@ -7,12 +7,15 @@ responsible for the recalculation should return a new object with the necessary modifications. """ + import re + from enum import Enum from functools import total_ordering from .exceptions import CaptionReadSyntaxError, RelativizationError + _UNIT_MISMATCH_MSG = "The sizes should have the same measure units." diff --git a/pycaption/microdvd.py b/pycaption/microdvd.py index a2619a3e..e58e1db9 100644 --- a/pycaption/microdvd.py +++ b/pycaption/microdvd.py @@ -5,6 +5,7 @@ """ import re + from copy import deepcopy from .base import ( diff --git a/pycaption/sami/constants.py b/pycaption/sami/constants.py index f09f2a79..36c04ba6 100644 --- a/pycaption/sami/constants.py +++ b/pycaption/sami/constants.py @@ -10,6 +10,7 @@ from ..geometry import HorizontalAlignmentEnum + log.setLevel(FATAL) SAMI_BASE_MARKUP = """ diff --git a/pycaption/sami/reader.py b/pycaption/sami/reader.py index 0b7d19c7..bc4a7601 100644 --- a/pycaption/sami/reader.py +++ b/pycaption/sami/reader.py @@ -11,6 +11,7 @@ from ..geometry import Alignment, HorizontalAlignmentEnum, Layout, Padding, Size from .parser import SAMIParser + _TAG_TO_STYLE = {"i": "italics", "b": "bold", "u": "underline"} diff --git a/pycaption/sami/writer.py b/pycaption/sami/writer.py index ceb9c099..d2dda8a8 100644 --- a/pycaption/sami/writer.py +++ b/pycaption/sami/writer.py @@ -13,6 +13,7 @@ from ..geometry import HorizontalAlignmentEnum from .constants import HORIZONTAL_ALIGNMENT_MAP, SAMI_BASE_MARKUP + _NON_CSS_KEYS = frozenset( { "classes", diff --git a/pycaption/scc/__init__.py b/pycaption/scc/__init__.py index 5b76c046..fd3040de 100644 --- a/pycaption/scc/__init__.py +++ b/pycaption/scc/__init__.py @@ -3,4 +3,5 @@ from .reader import SCCReader from .writer import SCC_TOKENS_PER_CAPTION_MAX, SCCWriter + __all__ = ["SCCReader", "SCCWriter", "SCC_TOKENS_PER_CAPTION_MAX"] diff --git a/pycaption/scc/constants.py b/pycaption/scc/constants.py index 9f95f656..5e98c49a 100644 --- a/pycaption/scc/constants.py +++ b/pycaption/scc/constants.py @@ -7,8 +7,10 @@ """ import re as _re + from itertools import product + COMMANDS = { "9420": "", "9429": "", diff --git a/pycaption/scc/reader.py b/pycaption/scc/reader.py index 93a182e7..b9aea77b 100644 --- a/pycaption/scc/reader.py +++ b/pycaption/scc/reader.py @@ -78,6 +78,7 @@ """ import re + from collections import deque from copy import deepcopy @@ -108,6 +109,7 @@ ) from .state_machines import DefaultProvidingPositionTracker + _TIMECODE_RE = re.compile(r"\d{2}:\d{2}:\d{2}[:;](\d{1,2})") diff --git a/pycaption/scc/specialized_collections.py b/pycaption/scc/specialized_collections.py index d10b6190..db85d29a 100644 --- a/pycaption/scc/specialized_collections.py +++ b/pycaption/scc/specialized_collections.py @@ -29,6 +29,7 @@ UNDERLINE_COMMANDS, ) + PopOnCue = collections.namedtuple("PopOnCue", "buffer, start, end") # First two hex chars of SCC codes that produce punctuation ['.', '!', '?', ','] diff --git a/pycaption/scc/writer.py b/pycaption/scc/writer.py index 3626b14b..72d5853f 100644 --- a/pycaption/scc/writer.py +++ b/pycaption/scc/writer.py @@ -6,6 +6,7 @@ import math import textwrap + from copy import deepcopy from pycaption.base import BaseWriter, CaptionNode @@ -26,6 +27,7 @@ WRITER_PAC_CODES, ) + SCC_TOKENS_PER_CAPTION_MAX = 80 _SCC_PREFIX = ["94ae", "94ae", "9420", "9420"] @@ -237,7 +239,7 @@ def _render_pop_on(self, code, start, ts): code_tokens = code.split() if len(code_tokens) <= max_payload: - return f"{ts}\t" "94ae 94ae 9420 9420 " f"{code}" "942c 942c 942f 942f\n\n" + return f"{ts}\t94ae 94ae 9420 9420 {code}942c 942c 942f 942f\n\n" output = "" offset = 0 @@ -273,9 +275,7 @@ def _render_roll_up(code, ts, depth, prev_mode): def _render_paint_on(code, ts): """Render a paint-on cue.""" return ( - f"{ts}\t" - f"{_RESUME_DIRECT_CAPTIONING} {_RESUME_DIRECT_CAPTIONING} " - f"{code}\n\n" + f"{ts}\t{_RESUME_DIRECT_CAPTIONING} {_RESUME_DIRECT_CAPTIONING} {code}\n\n" ) @staticmethod diff --git a/pycaption/scripts/caption_preview/caption_preview.py b/pycaption/scripts/caption_preview/caption_preview.py index 1887ff85..5d7cbd41 100644 --- a/pycaption/scripts/caption_preview/caption_preview.py +++ b/pycaption/scripts/caption_preview/caption_preview.py @@ -49,8 +49,12 @@ def end_headers(self): def main(): parser = argparse.ArgumentParser(description="Caption preview server") - parser.add_argument("directory", nargs="?", default=".", - help="Directory to serve media/caption files from") + parser.add_argument( + "directory", + nargs="?", + default=".", + help="Directory to serve media/caption files from", + ) parser.add_argument("--port", type=int, default=8080) args = parser.parse_args() diff --git a/pycaption/srt.py b/pycaption/srt.py index 28bf505f..efccc36c 100644 --- a/pycaption/srt.py +++ b/pycaption/srt.py @@ -3,7 +3,12 @@ from copy import deepcopy from .base import ( - BaseReader, BaseWriter, Caption, CaptionList, CaptionNode, CaptionSet, + BaseReader, + BaseWriter, + Caption, + CaptionList, + CaptionNode, + CaptionSet, merge_caption_list, ) from .exceptions import CaptionReadNoCaptions, CaptionReadSyntaxError diff --git a/pycaption/webvtt/__init__.py b/pycaption/webvtt/__init__.py index 801b37cc..7b9c3a2d 100644 --- a/pycaption/webvtt/__init__.py +++ b/pycaption/webvtt/__init__.py @@ -9,4 +9,5 @@ from .reader import WebVTTReader from .writer import WebVTTWriter + __all__ = ["WebVTTReader", "WebVTTWriter", "microseconds"] diff --git a/pycaption/webvtt/constants.py b/pycaption/webvtt/constants.py index c5626bb5..94e62f7d 100644 --- a/pycaption/webvtt/constants.py +++ b/pycaption/webvtt/constants.py @@ -8,6 +8,7 @@ from ..geometry import HorizontalAlignmentEnum, LineAlignmentEnum, PositionAlignmentEnum + TIMING_LINE_PATTERN = re.compile(r"^(\S+)\s+-->\s+(\S+)(?:\s+(.*?))?\s*$") """ Captures [start_timestamp], [end_timestamp], and optional [cue_settings] diff --git a/pycaption/webvtt/writer.py b/pycaption/webvtt/writer.py index b7f5273a..8fe0da34 100644 --- a/pycaption/webvtt/writer.py +++ b/pycaption/webvtt/writer.py @@ -12,6 +12,7 @@ from ..geometry import WritingDirectionEnum from .constants import DEFAULT_ALIGN, WEBVTT_VERSION_OF + _SIMPLE_STRUCTURAL_TAGS = { "ruby": ("", ""), "ruby_text": ("", ""), diff --git a/run_tests.sh b/run_tests.sh index 71abf561..f0be057c 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -25,5 +25,3 @@ if [ $? != 0 ]; then else cleanup fi - - diff --git a/setup.py b/setup.py index c31e46b0..25055aa5 100644 --- a/setup.py +++ b/setup.py @@ -3,6 +3,7 @@ from setuptools import find_packages, setup + README_PATH = os.path.join( os.path.abspath(os.path.dirname(__file__)), "README.rst", @@ -28,7 +29,7 @@ project_urls={ "Source": "https://github.com/pbs/pycaption", "Documentation": "https://pycaption.readthedocs.io/", - "Release notes": "https://pycaption.readthedocs.io" "/en/stable/changelog.html", + "Release notes": "https://pycaption.readthedocs.io/en/stable/changelog.html", }, python_requires=">=3.10,<4.0", install_requires=dependencies, diff --git a/test_requirements.txt b/test_requirements.txt index c9532a43..1a592de5 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -1,5 +1,5 @@ -pytest -pytest-cov beautifulsoup4>=4.12.1 +cssutils>=2.0.0 lxml>=4.9.1 -cssutils>=2.0.0 \ No newline at end of file +pytest +pytest-cov diff --git a/tests/conftest.py b/tests/conftest.py index 13af5f21..9d3f6aa6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -50,16 +50,16 @@ sample_dfxp_with_templated_style, sample_dfxp_without_region_and_style, ) -from tests.fixtures.microdvd import missing_fps_sample_microdvd # noqa: F401 from tests.fixtures.microdvd import ( # noqa: F401 + missing_fps_sample_microdvd, sample_microdvd, sample_microdvd_2, sample_microdvd_empty, sample_microdvd_empty_cue_output, sample_microdvd_invalid_format, ) -from tests.fixtures.sami import sample_sami # noqa: F401 from tests.fixtures.sami import ( # noqa: F401 + sample_sami, sample_sami_double_br, sample_sami_empty, sample_sami_empty_cue_output, @@ -141,9 +141,9 @@ sample_scc_with_unknown_commands, scc_that_generates_webvtt_with_proper_newlines, ) -from tests.fixtures.srt import sample_srt_ascii # noqa: F401 from tests.fixtures.srt import ( # noqa: F401 sample_srt, + sample_srt_ascii, sample_srt_blank_lines, sample_srt_empty, sample_srt_empty_cue_output, diff --git a/tests/mixins.py b/tests/mixins.py index f1a02630..c3b6ae41 100644 --- a/tests/mixins.py +++ b/tests/mixins.py @@ -1,6 +1,7 @@ import re import pytest + from bs4 import BeautifulSoup from pycaption.exceptions import InvalidInputError diff --git a/tests/test_bytes_input.py b/tests/test_bytes_input.py index 2d292fe4..7d89a04c 100644 --- a/tests/test_bytes_input.py +++ b/tests/test_bytes_input.py @@ -157,5 +157,3 @@ def test_read_bytes_with_bom(self, sample_microdvd): def test_detect_bytes(self, sample_microdvd): assert self.reader.detect(sample_microdvd.encode("utf-8")) is True - - diff --git a/tests/test_dfxp_conversion.py b/tests/test_dfxp_conversion.py index 5ad336cd..70dd3b08 100644 --- a/tests/test_dfxp_conversion.py +++ b/tests/test_dfxp_conversion.py @@ -19,6 +19,7 @@ from .mixins import DFXPTestingMixIn, MicroDVDTestingMixIn, WebVTTTestingMixIn + # Arbitrary values used to test relativization VIDEO_WIDTH = 640 VIDEO_HEIGHT = 360 @@ -242,9 +243,9 @@ def test_input_inline_positioning_output_default_alignment_is_start( results = WebVTTWriter(video_width=640, video_height=360).write(caption_set) start_align_count = results.count("align:start") - assert ( - start_align_count == 3 - ), f"{3 - start_align_count} default alignment(s) missing." + assert start_align_count == 3, ( + f"{3 - start_align_count} default alignment(s) missing." + ) class TestDFXPtoMicroDVD(MicroDVDTestingMixIn): diff --git a/tests/test_double_encoding.py b/tests/test_double_encoding.py index 2cad3d72..cfcb1548 100644 --- a/tests/test_double_encoding.py +++ b/tests/test_double_encoding.py @@ -41,9 +41,7 @@ class TestDoubleEncodingEndToEnd: def test_srt_reader(self): garbled_note = _double_encode("♪") content = ( - "1\n" - "00:00:01,000 --> 00:00:02,000\n" - f"{garbled_note} Music {garbled_note}\n" + f"1\n00:00:01,000 --> 00:00:02,000\n{garbled_note} Music {garbled_note}\n" ) captions = SRTReader().read(content) nodes = captions.get_captions("en-US")[0].nodes diff --git a/tests/test_sami.py b/tests/test_sami.py index 8d96ebac..0811ed1b 100644 --- a/tests/test_sami.py +++ b/tests/test_sami.py @@ -52,20 +52,14 @@ def test_missing_start(self, sample_sami_missing_start): @pytest.mark.parametrize("start", ["abc", "1rt=1000", ""]) def test_non_numeric_start_raises_timing_error(self, start): - content = ( - f"" - "

hi

" - ) + content = f"

hi

" with pytest.raises(CaptionReadTimingError): self.reader.read(content) def test_valueless_class_attribute_is_ignored(self): # A bare ``class`` attribute (no value) used to crash _find_lang with # AttributeError; it should just be treated as carrying no language. - content = ( - "" - "

hi

" - ) + content = "

hi

" caption_set = self.reader.read(content) langs = caption_set.get_languages() assert caption_set.get_captions(langs[0])[0].get_text() == "hi" diff --git a/tests/test_sami_conversion.py b/tests/test_sami_conversion.py index 33e493b2..49e62c93 100644 --- a/tests/test_sami_conversion.py +++ b/tests/test_sami_conversion.py @@ -9,6 +9,7 @@ from .mixins import SAMITestingMixIn + # Arbitrary values used to test relativization VIDEO_WIDTH = 640 VIDEO_HEIGHT = 360 @@ -148,18 +149,14 @@ def test_multiple_css_properties(self): assert "color:red;" in result or "color: red;" in result def test_positioning_alignment(self): - vtt = ( - "WEBVTT\n\n" "00:00:01.000 --> 00:00:04.000 align:right\n" "Right aligned\n" - ) + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:04.000 align:right\nRight aligned\n" caption_set = WebVTTReader().read(vtt) result = SAMIWriter().write(caption_set) assert "text-align:right;" in result def test_writing_direction_dropped(self): - vtt = ( - "WEBVTT\n\n" "00:00:01.000 --> 00:00:04.000 vertical:rl\n" "Vertical text\n" - ) + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:04.000 vertical:rl\nVertical text\n" caption_set = WebVTTReader().read(vtt) result = SAMIWriter().write(caption_set) @@ -168,7 +165,7 @@ def test_writing_direction_dropped(self): assert "Vertical text" in result def test_plain_cue_regression(self): - vtt = "WEBVTT\n\n" "00:00:01.000 --> 00:00:04.000\n" "Plain caption text\n" + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:04.000\nPlain caption text\n" caption_set = WebVTTReader().read(vtt) result = SAMIWriter().write(caption_set) diff --git a/tests/test_scc.py b/tests/test_scc.py index 82407a32..0c03a019 100644 --- a/tests/test_scc.py +++ b/tests/test_scc.py @@ -11,6 +11,7 @@ from pycaption.scc.state_machines import DefaultProvidingPositionTracker from tests.mixins import ReaderTestingMixIn + TOLERANCE_MICROSECONDS = 500 * 1000 @@ -106,9 +107,9 @@ def test_breaks_do_not_accumulate_across_caption_boundaries( "Caption must not start with a BREAK node — indicates breaks " "leaked from the previous caption" ) - assert ( - caption.nodes[-1].type_ != CaptionNode.BREAK - ), "Caption must not end with a trailing BREAK node" + assert caption.nodes[-1].type_ != CaptionNode.BREAK, ( + "Caption must not end with a trailing BREAK node" + ) def test_row_jump_with_pending_reposition_creates_new_cue( self, sample_scc_row_jump_with_pending_reposition @@ -131,12 +132,12 @@ def test_row_jump_with_pending_reposition_creates_new_cue( assert first.layout_info.origin != second.layout_info.origin for caption in captions: - assert ( - caption.nodes[0].type_ != CaptionNode.BREAK - ), "Cue must not start with a phantom BREAK node" - assert not any( - node.type_ == CaptionNode.BREAK for node in caption.nodes - ), "Cue must not contain any phantom BREAK node" + assert caption.nodes[0].type_ != CaptionNode.BREAK, ( + "Cue must not start with a phantom BREAK node" + ) + assert not any(node.type_ == CaptionNode.BREAK for node in caption.nodes), ( + "Cue must not contain any phantom BREAK node" + ) def test_row_skip_creates_new_cue_in_paint_on_mode( self, sample_scc_paint_on_row_and_column_jump_in_one_pac @@ -164,9 +165,9 @@ def test_row_skip_creates_new_cue_in_paint_on_mode( assert first.layout_info.origin != second.layout_info.origin for caption in captions: - assert not any( - node.type_ == CaptionNode.BREAK for node in caption.nodes - ), "Cue must not contain any phantom BREAK node" + assert not any(node.type_ == CaptionNode.BREAK for node in caption.nodes), ( + "Cue must not contain any phantom BREAK node" + ) def test_paint_on_row_plus_one_large_column_jump_creates_new_cue( self, sample_scc_paint_on_row_plus_one_large_column_jump @@ -195,9 +196,9 @@ def test_paint_on_row_plus_one_large_column_jump_creates_new_cue( assert first.layout_info.origin != second.layout_info.origin for caption in captions: - assert not any( - node.type_ == CaptionNode.BREAK for node in caption.nodes - ), "Cue must not contain any phantom BREAK node" + assert not any(node.type_ == CaptionNode.BREAK for node in caption.nodes), ( + "Cue must not contain any phantom BREAK node" + ) def test_row_plus_one_large_column_jump_stays_one_cue_in_pop_on_mode( self, sample_scc_pop_on_row_plus_one_large_column_jump @@ -222,9 +223,9 @@ def test_row_plus_one_large_column_jump_stays_one_cue_in_pop_on_mode( "AB", "CD", ] - assert any( - node.type_ == CaptionNode.BREAK for node in caption.nodes - ), "Cue must join the two PACs with a BREAK node, not split into two cues" + assert any(node.type_ == CaptionNode.BREAK for node in caption.nodes), ( + "Cue must join the two PACs with a BREAK node, not split into two cues" + ) def test_row_plus_one_large_column_jump_stays_one_cue_in_roll_up_mode( self, sample_scc_roll_up_row_plus_one_large_column_jump @@ -245,9 +246,9 @@ def test_row_plus_one_large_column_jump_stays_one_cue_in_roll_up_mode( "AB", "CD", ] - assert any( - node.type_ == CaptionNode.BREAK for node in caption.nodes - ), "Cue must join the two PACs with a BREAK node, not split into two cues" + assert any(node.type_ == CaptionNode.BREAK for node in caption.nodes), ( + "Cue must join the two PACs with a BREAK node, not split into two cues" + ) def test_row_skip_creates_new_cue_in_pop_on_mode( self, sample_scc_pop_on_row_and_column_jump_in_one_pac @@ -273,9 +274,9 @@ def test_row_skip_creates_new_cue_in_pop_on_mode( assert first.layout_info.origin != second.layout_info.origin for caption in captions: - assert not any( - node.type_ == CaptionNode.BREAK for node in caption.nodes - ), "Cue must not contain any phantom BREAK node" + assert not any(node.type_ == CaptionNode.BREAK for node in caption.nodes), ( + "Cue must not contain any phantom BREAK node" + ) def test_row_skip_creates_new_cue_in_roll_up_mode( self, sample_scc_roll_up_row_and_column_jump_in_one_pac @@ -297,9 +298,9 @@ def test_row_skip_creates_new_cue_in_roll_up_mode( assert first.layout_info.origin != second.layout_info.origin for caption in captions: - assert not any( - node.type_ == CaptionNode.BREAK for node in caption.nodes - ), "Cue must not contain any phantom BREAK node" + assert not any(node.type_ == CaptionNode.BREAK for node in caption.nodes), ( + "Cue must not contain any phantom BREAK node" + ) def test_row_skip_does_not_preserve_blank_line( self, sample_scc_row_skip_does_not_preserve_blank_line @@ -329,9 +330,9 @@ def test_row_skip_does_not_preserve_blank_line( assert first.layout_info.origin != second.layout_info.origin for caption in captions: - assert not any( - node.type_ == CaptionNode.BREAK for node in caption.nodes - ), "Cue must not contain any phantom BREAK node" + assert not any(node.type_ == CaptionNode.BREAK for node in caption.nodes), ( + "Cue must not contain any phantom BREAK node" + ) def test_tab_offset(self, sample_scc_tab_offset): captions = SCCReader().read(sample_scc_tab_offset) diff --git a/tests/test_scc_conversion.py b/tests/test_scc_conversion.py index 0d618f4f..533b2a32 100644 --- a/tests/test_scc_conversion.py +++ b/tests/test_scc_conversion.py @@ -13,6 +13,7 @@ ) from tests.mixins import CaptionSetTestingMixIn + # This is quite fuzzy at the moment. TOLERANCE_MICROSECONDS = 600 * 1000 @@ -188,9 +189,9 @@ def test_scc_captions_are_in_order_when_short_text_followed_by_long(self): # SCC timestamps use HH:MM:SS:FF format (FF = frames) timestamps = re.findall(r"(\d+:\d+:\d+:\d+)", scc_output) for i in range(1, len(timestamps)): - assert ( - timestamps[i] >= timestamps[i - 1] - ), f"Timestamps out of order: {timestamps[i - 1]} > {timestamps[i]}" + assert timestamps[i] >= timestamps[i - 1], ( + f"Timestamps out of order: {timestamps[i - 1]} > {timestamps[i]}" + ) class TestSCCToWebVTT: diff --git a/tests/test_scc_translator.py b/tests/test_scc_translator.py index 3ba48fb9..6953121e 100644 --- a/tests/test_scc_translator.py +++ b/tests/test_scc_translator.py @@ -22,9 +22,7 @@ def test_custom_brackets( assert sample_translated_scc_custom_brackets == result def test_commands_not_found( - self, - sample_scc_with_unknown_commands, - sample_translated_scc_commands_not_found + self, sample_scc_with_unknown_commands, sample_translated_scc_commands_not_found ): result = translate_scc(sample_scc_with_unknown_commands) diff --git a/tests/test_scc_writer.py b/tests/test_scc_writer.py index 8ad8130a..4c2c9c0e 100644 --- a/tests/test_scc_writer.py +++ b/tests/test_scc_writer.py @@ -99,9 +99,9 @@ def test_timestamps_monotonically_increasing_ndf(self): output = SCCWriter(drop_frame=False).write(captions) timestamps = re.findall(r"(\d{2}:\d{2}:\d{2}:\d{2})", output) for i in range(1, len(timestamps)): - assert ( - timestamps[i] >= timestamps[i - 1] - ), f"NDF timestamps out of order: {timestamps[i - 1]} > {timestamps[i]}" + assert timestamps[i] >= timestamps[i - 1], ( + f"NDF timestamps out of order: {timestamps[i - 1]} > {timestamps[i]}" + ) def test_timestamps_monotonically_increasing_df(self): vtt_input = ( @@ -121,9 +121,9 @@ def test_timestamps_monotonically_increasing_df(self): output = SCCWriter(drop_frame=True).write(captions) timestamps = re.findall(r"(\d{2}:\d{2}:\d{2};\d{2})", output) for i in range(1, len(timestamps)): - assert ( - timestamps[i] >= timestamps[i - 1] - ), f"DF timestamps out of order: {timestamps[i - 1]} > {timestamps[i]}" + assert timestamps[i] >= timestamps[i - 1], ( + f"DF timestamps out of order: {timestamps[i - 1]} > {timestamps[i]}" + ) def test_rapid_short_captions_stay_ordered(self): """Short text followed by long text should not cause timestamp inversion.""" @@ -142,14 +142,14 @@ def test_rapid_short_captions_stay_ordered(self): pattern = r"\d{2}:\d{2}:\d{2}" + re.escape(sep) + r"\d{2}" timestamps = re.findall(pattern, output) for i in range(1, len(timestamps)): - assert ( - timestamps[i] >= timestamps[i - 1] - ), f"drop_frame={df}: {timestamps[i - 1]} > {timestamps[i]}" + assert timestamps[i] >= timestamps[i - 1], ( + f"drop_frame={df}: {timestamps[i - 1]} > {timestamps[i]}" + ) class TestSCCWriterFirstCueBackshift: def test_first_cue_start_is_shifted_back(self): - srt = "1\n" "00:00:10,000 --> 00:00:12,000\n" "Hello world\n" + srt = "1\n00:00:10,000 --> 00:00:12,000\nHello world\n" captions = SRTReader().read(srt) output = SCCWriter(drop_frame=False).write(captions) timestamps = re.findall(r"(\d{2}:\d{2}:\d{2}:\d{2})", output) @@ -158,7 +158,7 @@ def test_first_cue_start_is_shifted_back(self): assert timestamps[0] < "00:00:09:29" def test_first_cue_at_zero_does_not_go_negative(self): - srt = "1\n" "00:00:00,100 --> 00:00:02,000\n" "Hello\n" + srt = "1\n00:00:00,100 --> 00:00:02,000\nHello\n" captions = SRTReader().read(srt) output = SCCWriter(drop_frame=False).write(captions) timestamps = re.findall(r"(\d{2}:\d{2}:\d{2}:\d{2})", output) @@ -215,7 +215,7 @@ def test_split_caption_exceeding_80_tokens(self): """A caption that would exceed 80 SCC tokens should be split.""" # Create a very long caption that will produce many code tokens long_text = "A" * 32 + "\n" + "B" * 32 + "\n" + "C" * 32 + "\n" + "D" * 32 - srt = "1\n" "00:00:05,000 --> 00:00:10,000\n" f"{long_text}\n" + srt = f"1\n00:00:05,000 --> 00:00:10,000\n{long_text}\n" captions = SRTReader().read(srt) output = SCCWriter(drop_frame=False).write(captions) # Each output line (non-empty, non-header) should have <= 80 tokens @@ -287,48 +287,42 @@ def test_webvtt_to_scc_roundtrip_df(self): class TestSCCWriterPositioning: def test_vtt_line_top_maps_to_row_1(self): - vtt = "WEBVTT\n\n" "00:00:01.000 --> 00:00:03.000 line:0%\n" "Top of screen\n" + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000 line:0%\nTop of screen\n" captions = WebVTTReader().read(vtt) output = SCCWriter().write(captions) pac_row_1 = WRITER_PAC_CODES[(1, 0, "plain")] assert pac_row_1 in output def test_vtt_line_bottom_maps_to_row_15(self): - vtt = ( - "WEBVTT\n\n" - "00:00:01.000 --> 00:00:03.000 line:100%\n" - "Bottom of screen\n" - ) + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000 line:100%\nBottom of screen\n" captions = WebVTTReader().read(vtt) output = SCCWriter().write(captions) pac_row_15 = WRITER_PAC_CODES[(15, 0, "plain")] assert pac_row_15 in output def test_vtt_line_middle_maps_to_row_9(self): - vtt = ( - "WEBVTT\n\n" "00:00:01.000 --> 00:00:03.000 line:50%\n" "Middle of screen\n" - ) + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000 line:50%\nMiddle of screen\n" captions = WebVTTReader().read(vtt) output = SCCWriter().write(captions) pac_row_9 = WRITER_PAC_CODES[(9, 0, "plain")] assert pac_row_9 in output def test_vtt_no_position_defaults_to_bottom(self): - vtt = "WEBVTT\n\n" "00:00:01.000 --> 00:00:03.000\n" "Default position\n" + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000\nDefault position\n" captions = WebVTTReader().read(vtt) output = SCCWriter().write(captions) pac_row_15 = WRITER_PAC_CODES[(15, 0, "plain")] assert pac_row_15 in output def test_vtt_align_left_indent_zero(self): - vtt = "WEBVTT\n\n" "00:00:01.000 --> 00:00:03.000 align:left\n" "Left aligned\n" + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000 align:left\nLeft aligned\n" captions = WebVTTReader().read(vtt) output = SCCWriter().write(captions) pac_col_0 = WRITER_PAC_CODES[(15, 0, "plain")] assert pac_col_0 in output def test_vtt_align_center_computes_indent(self): - vtt = "WEBVTT\n\n" "00:00:01.000 --> 00:00:03.000 align:center\n" "Hi\n" + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000 align:center\nHi\n" captions = WebVTTReader().read(vtt) output = SCCWriter().write(captions) # "Hi" is 2 chars, center = (32-2)//2 = 15, base_col=12, tab=3 @@ -336,7 +330,7 @@ def test_vtt_align_center_computes_indent(self): assert pac_col_12 in output def test_vtt_align_right_computes_indent(self): - vtt = "WEBVTT\n\n" "00:00:01.000 --> 00:00:03.000 align:right\n" "Hi\n" + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000 align:right\nHi\n" captions = WebVTTReader().read(vtt) output = SCCWriter().write(captions) # "Hi" is 2 chars, right = 32-2 = 30 -> clamped to 28 @@ -383,33 +377,31 @@ def test_three_line_at_86_percent_stacks_upward(self): class TestSCCWriterStyles: def test_vtt_italic_emits_mid_row_code(self): - vtt = "WEBVTT\n\n" "00:00:01.000 --> 00:00:03.000\n" "Hello world\n" + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000\nHello world\n" captions = WebVTTReader().read(vtt) output = SCCWriter().write(captions) assert MID_ROW_ITALIC in output def test_vtt_underline_emits_mid_row_code(self): - vtt = "WEBVTT\n\n" "00:00:01.000 --> 00:00:03.000\n" "Hello world\n" + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000\nHello world\n" captions = WebVTTReader().read(vtt) output = SCCWriter().write(captions) assert MID_ROW_UNDERLINE in output def test_vtt_italic_underline_combined(self): - vtt = ( - "WEBVTT\n\n" "00:00:01.000 --> 00:00:03.000\n" "Hello world\n" - ) + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000\nHello world\n" captions = WebVTTReader().read(vtt) output = SCCWriter().write(captions) assert MID_ROW_ITALIC_UNDERLINE in output def test_vtt_italic_ends_with_plain_code(self): - vtt = "WEBVTT\n\n" "00:00:01.000 --> 00:00:03.000\n" "hello world\n" + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000\nhello world\n" captions = WebVTTReader().read(vtt) output = SCCWriter().write(captions) assert MID_ROW_PLAIN in output def test_vtt_bold_silently_dropped(self): - vtt = "WEBVTT\n\n" "00:00:01.000 --> 00:00:03.000\n" "bold text\n" + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000\nbold text\n" captions = WebVTTReader().read(vtt) output = SCCWriter().write(captions) assert MID_ROW_ITALIC not in output @@ -419,11 +411,7 @@ def test_vtt_bold_silently_dropped(self): assert not result.is_empty() def test_vtt_class_span_silently_dropped(self): - vtt = ( - "WEBVTT\n\n" - "00:00:01.000 --> 00:00:03.000\n" - "colored text\n" - ) + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000\ncolored text\n" captions = WebVTTReader().read(vtt) output = SCCWriter().write(captions) assert MID_ROW_ITALIC not in output @@ -431,7 +419,7 @@ def test_vtt_class_span_silently_dropped(self): assert not result.is_empty() def test_vtt_italic_at_line_start_uses_pac(self): - vtt = "WEBVTT\n\n" "00:00:01.000 --> 00:00:03.000\n" "all italic\n" + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000\nall italic\n" captions = WebVTTReader().read(vtt) output = SCCWriter().write(captions) pac_italic = WRITER_PAC_CODES[(15, 0, "italic")] @@ -468,11 +456,7 @@ def test_plain_text_roundtrip_unchanged(self): assert "Second caption" in caps[1].get_text() def test_multiline_with_position(self): - vtt = ( - "WEBVTT\n\n" - "00:00:01.000 --> 00:00:03.000 line:20%\n" - "Line one\nLine two\n" - ) + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000 line:20%\nLine one\nLine two\n" captions = WebVTTReader().read(vtt) output = SCCWriter().write(captions) result = SCCReader().read(output) @@ -582,9 +566,7 @@ def test_scroll_text_survives_roundtrip(self): class TestSCCWriterPaintOn: def test_paint_on_emits_rdc_preamble(self): scc = ( - "Scenarist_SCC V1.0\n\n" - "00:00:00;00\t9429 54e5 73f4\n\n" - "00:00:04;00\t942c\n\n" + "Scenarist_SCC V1.0\n\n00:00:00;00\t9429 54e5 73f4\n\n00:00:04;00\t942c\n\n" ) captions = SCCReader().read(scc) output = SCCWriter().write(captions) @@ -593,9 +575,7 @@ def test_paint_on_emits_rdc_preamble(self): def test_paint_on_text_survives_roundtrip(self): scc = ( - "Scenarist_SCC V1.0\n\n" - "00:00:00;00\t9429 54e5 73f4\n\n" - "00:00:04;00\t942c\n\n" + "Scenarist_SCC V1.0\n\n00:00:00;00\t9429 54e5 73f4\n\n00:00:04;00\t942c\n\n" ) captions = SCCReader().read(scc) output = SCCWriter().write(captions) diff --git a/tests/test_webvtt.py b/tests/test_webvtt.py index 132b380b..f782d37c 100644 --- a/tests/test_webvtt.py +++ b/tests/test_webvtt.py @@ -105,7 +105,7 @@ def test_not_ignoring_timing_errors(self): # todo: same assert w/ different arguments -> this can be parametrized; with pytest.raises(CaptionReadError): WebVTTReader(ignore_timing_errors=False).read( - "WEBVTT\n\n" "00:00:20.000 --> 00:00:10.000\n" "foo bar baz" + "WEBVTT\n\n00:00:20.000 --> 00:00:10.000\nfoo bar baz" ) with pytest.raises(CaptionReadError): @@ -129,9 +129,7 @@ def test_ignoring_timing_errors(self): # Even if timing errors are ignored, this has to raise an exception with pytest.raises(CaptionReadSyntaxError): WebVTTReader().read( - "WEBVTT\n\n" - "NOTE invalid cue stamp\n\n" - "00:00:20.000 --> \nfoo bar baz\n" + "WEBVTT\n\nNOTE invalid cue stamp\n\n00:00:20.000 --> \nfoo bar baz\n" ) # And this too @@ -311,9 +309,7 @@ def test_region_webvtt_positioning_passthrough(self): assert cue.layout_info.webvtt_positioning == "region:r1" def test_invalid_region_reference_ignored(self): - vtt = ( - "WEBVTT\n\n" "00:00:01.000 --> 00:00:03.000 region:nonexistent\n" "Hello\n" - ) + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000 region:nonexistent\nHello\n" captions = self.reader.read(vtt) cue = captions.get_captions("en-US")[0] # Falls back to raw positioning passthrough @@ -589,7 +585,7 @@ def test_full_caption_read_with_structural_tags( def test_multiline_cue_with_style_spanning_lines(self): from pycaption.base import CaptionNode - vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000\n" "line one\nline two\n" + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000\nline one\nline two\n" captions = self.reader.read(vtt) cue = captions.get_captions("en-US")[0] nodes = cue.nodes @@ -697,7 +693,7 @@ def test_partially_closed_tags_only_unclosed_auto_closed(self): def test_unclosed_tag_spanning_multiline_cue(self): from pycaption.base import CaptionNode - vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000\n" "line one\nline two\n" + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000\nline one\nline two\n" captions = self.reader.read(vtt) cue = captions.get_captions("en-US")[0] nodes = cue.nodes @@ -856,11 +852,7 @@ def test_position_alignment_line_right_stored(self): assert layout.origin.x == Size(70, UnitEnum.PERCENT) def test_position_alignment_none_when_unspecified(self): - vtt = ( - "WEBVTT\n\n" - "00:00:01.000 --> 00:00:03.000 position:50% line:80%\n" - "Hello\n" - ) + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000 position:50% line:80%\nHello\n" captions = self.reader.read(vtt) cue = captions.get_captions("en-US")[0] layout = cue.layout_info @@ -958,7 +950,7 @@ def test_multiple_cues_with_different_position_alignments(self): assert cues[2].layout_info.origin.x == Size(20.0, UnitEnum.PERCENT) def test_line_with_alignment_subvalue(self): - vtt = "WEBVTT\n\n" "00:00:01.000 --> 00:00:03.000 line:80%,center\n" "Hello\n" + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000 line:80%,center\nHello\n" captions = self.reader.read(vtt) cue = captions.get_captions("en-US")[0] layout = cue.layout_info @@ -1032,10 +1024,7 @@ def test_position_only_sami_output_has_margin_left(self): def test_position_only_no_false_overflow_warning(self): vtt = ( - "WEBVTT\n\n" - "00:00:01.000 --> 00:00:03.000 position:70%\n" - "Line one\n" - "Line two\n" + "WEBVTT\n\n00:00:01.000 --> 00:00:03.000 position:70%\nLine one\nLine two\n" ) with warnings.catch_warnings(record=True) as caught: warnings.simplefilter("always") @@ -1657,7 +1646,7 @@ def test_warns_on_overflow(self): assert "3 lines" in str(caption_warnings[0].message) def test_no_warning_within_bounds(self): - vtt = "WEBVTT\n\n" "00:00:01.000 --> 00:00:03.000 line:50%\n" "Single line\n" + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000 line:50%\nSingle line\n" with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") self.reader.read(vtt) @@ -1668,11 +1657,7 @@ def test_no_warning_within_bounds(self): def test_no_warning_without_line_setting(self): vtt = ( - "WEBVTT\n\n" - "00:00:01.000 --> 00:00:03.000\n" - "Line one\n" - "Line two\n" - "Line three\n" + "WEBVTT\n\n00:00:01.000 --> 00:00:03.000\nLine one\nLine two\nLine three\n" ) with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") @@ -1684,12 +1669,7 @@ def test_no_warning_without_line_setting(self): def test_no_warning_at_boundary(self): # line:80% + 2 lines × 6.67% = 93.3% — within bounds - vtt = ( - "WEBVTT\n\n" - "00:00:01.000 --> 00:00:03.000 line:80%\n" - "Line one\n" - "Line two\n" - ) + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000 line:80%\nLine one\nLine two\n" with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") self.reader.read(vtt) @@ -1750,7 +1730,7 @@ def test_multiple_regions(self): assert "width:80%" in result def test_no_region_block_when_none_defined(self): - vtt = "WEBVTT\n\n" "00:00:01.000 --> 00:00:03.000\n" "Hello\n" + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000\nHello\n" caption_set = self.reader.read(vtt) result = WebVTTWriter().write(caption_set) assert "REGION" not in result diff --git a/tests/test_webvtt_conversion.py b/tests/test_webvtt_conversion.py index 0c40f898..8f0f0a62 100644 --- a/tests/test_webvtt_conversion.py +++ b/tests/test_webvtt_conversion.py @@ -150,13 +150,13 @@ def test_nested_style_roundtrip(self): assert "both" in result def test_class_roundtrip(self): - vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000\n" "colored\n" + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000\ncolored\n" caption_set = WebVTTReader().read(vtt) result = WebVTTWriter().write(caption_set) assert "colored" in result def test_lang_roundtrip(self): - vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000\n" "Bonjour\n" + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000\nBonjour\n" caption_set = WebVTTReader().read(vtt) result = WebVTTWriter().write(caption_set) assert "Bonjour" in result @@ -171,7 +171,7 @@ def test_ruby_roundtrip(self): assert "baseannotation" in result def test_timestamp_roundtrip(self): - vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:05.000\n" "Hello <00:00:02.500>world\n" + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:05.000\nHello <00:00:02.500>world\n" caption_set = WebVTTReader().read(vtt) result = WebVTTWriter().write(caption_set) assert "<00:00:02.500>" in result @@ -188,15 +188,13 @@ def test_mixed_style_and_text_roundtrip(self): assert "Normal italic bold end" in result def test_multiline_style_spanning_lines_roundtrip(self): - vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000\n" "line one\nline two\n" + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000\nline one\nline two\n" caption_set = WebVTTReader().read(vtt) result = WebVTTWriter().write(caption_set) assert "line one\nline two" in result def test_unrecognized_tag_preserved_roundtrip(self): - vtt = ( - "WEBVTT\n\n00:00:01.000 --> 00:00:03.000\n" "He said something\n" - ) + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000\nHe said something\n" caption_set = WebVTTReader().read(vtt) result = WebVTTWriter().write(caption_set) assert "LAUGHING" in result @@ -204,7 +202,7 @@ def test_unrecognized_tag_preserved_roundtrip(self): assert "something" in result def test_writer_encodes_illegal_characters(self): - vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000\n" "A & B < C\n" + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000\nA & B < C\n" caption_set = WebVTTReader().read(vtt) result = WebVTTWriter().write(caption_set) assert "A & B < C" in result @@ -212,7 +210,7 @@ def test_writer_encodes_illegal_characters(self): assert WebVTTWriter._encode_illegal_characters("-->") == "-->" def test_writer_encodes_entities_nbsp_lrm_rlm(self): - vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000\n" "word gap ‎ltr ‏rtl\n" + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000\nword gap ‎ltr ‏rtl\n" caption_set = WebVTTReader().read(vtt) result = WebVTTWriter().write(caption_set) assert "word gap" in result @@ -228,9 +226,9 @@ def test_writer_timestamp_always_has_hours(self): parts = timing_line.split(" --> ") for ts in parts: ts_clean = ts.split()[0] - assert re.match( - r"\d{2}:\d{2}:\d{2}\.\d{3}$", ts_clean - ), f"Timestamp {ts_clean} missing hours component" + assert re.match(r"\d{2}:\d{2}:\d{2}\.\d{3}$", ts_clean), ( + f"Timestamp {ts_clean} missing hours component" + ) class TestWebVTTStyleCrossFormat: @@ -254,14 +252,14 @@ def test_italic_to_srt(self): assert "" not in result def test_structural_tags_stripped_in_srt(self): - vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000\n" "Hello\n" + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000\nHello\n" caption_set = WebVTTReader().read(vtt) result = SRTWriter().write(caption_set) assert "Bonjour\n" + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000\nBonjour\n" caption_set = WebVTTReader().read(vtt) result = DFXPWriter().write(caption_set) assert " 00:00:03.000 line:50% align:end\n" - "Hello world\n" + "WEBVTT\n\n00:00:01.000 --> 00:00:03.000 line:50% align:end\nHello world\n" ) caption_set = WebVTTReader().read(vtt) result = DFXPWriter().write(caption_set) @@ -304,14 +300,14 @@ def test_vtt_roundtrip_preserves_positioning(self): assert "position:50% line:80% align:center" in result def test_line_alignment_center_to_dfxp(self): - vtt = "WEBVTT\n\n" "00:00:01.000 --> 00:00:03.000 line:50%,center\n" "Hello\n" + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000 line:50%,center\nHello\n" caption_set = WebVTTReader().read(vtt) result = DFXPWriter().write(caption_set) assert 'tts:displayAlign="center"' in result def test_line_alignment_vtt_non_passthrough_roundtrip(self): - vtt = "WEBVTT\n\n" "00:00:01.000 --> 00:00:03.000 line:80%,center\n" "Hello\n" + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000 line:80%,center\nHello\n" caption_set = WebVTTReader().read(vtt) for caption in caption_set.get_captions("en-US"): caption.layout_info.webvtt_positioning = None @@ -320,7 +316,7 @@ def test_line_alignment_vtt_non_passthrough_roundtrip(self): assert "line:80%,center" in result def test_line_alignment_absent_no_qualifier_in_vtt_output(self): - vtt = "WEBVTT\n\n" "00:00:01.000 --> 00:00:03.000 line:80%\n" "Hello\n" + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000 line:80%\nHello\n" caption_set = WebVTTReader().read(vtt) for caption in caption_set.get_captions("en-US"): caption.layout_info.webvtt_positioning = None @@ -430,7 +426,7 @@ def test_no_duplicate_vertical_with_passthrough(self): assert result.count("vertical:rl") == 1 def test_no_style_block_when_no_styles(self): - vtt = "WEBVTT\n\n" "00:00:01.000 --> 00:00:03.000\n" "Plain text\n" + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000\nPlain text\n" caption_set = WebVTTReader().read(vtt) result = WebVTTWriter().write(caption_set) @@ -490,11 +486,7 @@ def test_vertical_lr_to_dfxp_writing_mode(self): assert 'tts:writingMode="tblr"' in result def test_vertical_only_creates_region(self): - vtt = ( - "WEBVTT\n\n" - "00:00:01.000 --> 00:00:03.000 vertical:rl\n" - "Hello vertical\n" - ) + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000 vertical:rl\nHello vertical\n" caption_set = WebVTTReader().read(vtt) result = DFXPWriter().write(caption_set) @@ -514,9 +506,7 @@ def test_combined_positioning_and_writing_direction(self): assert "tts:origin" in result def test_horizontal_omits_writing_mode(self): - vtt = ( - "WEBVTT\n\n" "00:00:01.000 --> 00:00:03.000 line:50%\n" "Hello horizontal\n" - ) + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:03.000 line:50%\nHello horizontal\n" caption_set = WebVTTReader().read(vtt) result = DFXPWriter().write(caption_set) @@ -809,11 +799,7 @@ def test_class_override_not_double_wrapped(self): class TestYouTubeKaraokeEmptyClassRoundtrip: def test_empty_class_vtt_to_sami_roundtrip(self): - vtt = ( - "WEBVTT\n\n" - "00:00:01.000 --> 00:00:04.000\n" - " Hello world\n" - ) + vtt = "WEBVTT\n\n00:00:01.000 --> 00:00:04.000\n Hello world\n" caption_set = WebVTTReader().read(vtt) sami_output = SAMIWriter().write(caption_set) From a7aebafbf466e0a7c0394d4a46da06a388429308 Mon Sep 17 00:00:00 2001 From: "eduard.panasicov" Date: Mon, 14 Sep 2026 15:10:38 +0300 Subject: [PATCH 05/12] OCTO-11566 Ruff manual fixes --- pycaption/dfxp/extras.py | 2 +- pycaption/dfxp/reader.py | 2 +- pycaption/dfxp/writer.py | 4 ++-- pycaption/webvtt/reader.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pycaption/dfxp/extras.py b/pycaption/dfxp/extras.py index 37ffc7e6..383d3f1b 100644 --- a/pycaption/dfxp/extras.py +++ b/pycaption/dfxp/extras.py @@ -256,7 +256,7 @@ def _recreate_span(self, line, node, dfxp): return line - def _recreate_style(self, content, dfxp): + def _recreate_style(self, content, dfxp): # noqa: C901 """Convert an internal style dict to DFXP/TTS attributes. :type content: dict diff --git a/pycaption/dfxp/reader.py b/pycaption/dfxp/reader.py index cb52718e..25821c18 100644 --- a/pycaption/dfxp/reader.py +++ b/pycaption/dfxp/reader.py @@ -396,7 +396,7 @@ def _convert_span_to_nodes(self, tag): self._convert_tag_to_node(a) @staticmethod - def _convert_style(tag): + def _convert_style(tag): # noqa: C901 """Convert DFXP/TTS style attributes on a tag to an internal style dict. Maps tts:fontStyle, tts:fontWeight, tts:textDecoration, tts:textAlign, diff --git a/pycaption/dfxp/writer.py b/pycaption/dfxp/writer.py index 395d020f..e9261ffa 100644 --- a/pycaption/dfxp/writer.py +++ b/pycaption/dfxp/writer.py @@ -493,7 +493,7 @@ def _text_shadow_to_outline(value): return thickness -def _recreate_style(content, dfxp): +def _recreate_style(content, dfxp): # noqa: C901 """Convert an internal style dict to DFXP/TTS style attributes. Maps pycaption's internal keys (class, italics, bold, underline, color, @@ -544,7 +544,7 @@ def _recreate_style(content, dfxp): return dfxp_style -def _convert_layout_to_attributes(layout, fallback_alignment=None): +def _convert_layout_to_attributes(layout, fallback_alignment=None): # noqa: C901 """Convert a Layout object to a dict of DFXP region attributes. Maps origin, extent, padding, alignment, and writing_direction to their diff --git a/pycaption/webvtt/reader.py b/pycaption/webvtt/reader.py index 1a58b907..b0b1dab4 100644 --- a/pycaption/webvtt/reader.py +++ b/pycaption/webvtt/reader.py @@ -1038,7 +1038,7 @@ def _extract_cue_styles(css_text, styles): styles[key] = props @staticmethod - def _parse_css_declarations(declarations): + def _parse_css_declarations(declarations): # noqa: C901 """Parse CSS declaration text into pycaption's style dict. Maps recognized CSS properties to pycaption's internal style dict. From 0207dd9d309d3a9ab858268433dfdf9e3390e2bb Mon Sep 17 00:00:00 2001 From: "eduard.panasicov" Date: Mon, 14 Sep 2026 15:22:58 +0300 Subject: [PATCH 06/12] OCTO-11566 Update docs --- ai_artifacts/project_understanding.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ai_artifacts/project_understanding.md b/ai_artifacts/project_understanding.md index 7652dbd5..896c989b 100644 --- a/ai_artifacts/project_understanding.md +++ b/ai_artifacts/project_understanding.md @@ -15,7 +15,7 @@ and web caption formats. It is the caption ingestion engine behind PBS's video p | Language | Python (see `setup.py` for version bounds) | | Framework | None (pure library) | | Testing | pytest, pytest-lazy-fixture | -| Linting | flake8, pre-commit hooks | +| Linting | ruff (lint + format), pre-commit hooks | | Packaging | setuptools (setup.py) | | Dependencies | beautifulsoup4, lxml, cssutils | | Optional | nltk (transcript features) | @@ -91,7 +91,8 @@ pycaption/ ├── examples/ # Sample caption files ├── docs/ # Sphinx documentation (introduction.rst) ├── setup.py # Package config (version here) -├── .pre-commit-config.yaml # Linting: end-of-file-fixer, trailing-whitespace, flake8 +├── .pre-commit-config.yaml # Hook definitions (pre-commit-hooks + ruff) +├── ruff.toml # Ruff lint/format rules └── README.rst # Project readme ``` From e0912444c736f0f6e1906f78706a47a6f211cc29 Mon Sep 17 00:00:00 2001 From: "eduard.panasicov" Date: Mon, 14 Sep 2026 15:23:13 +0300 Subject: [PATCH 07/12] OCTO-11566 Remove unused setup.cfg --- setup.cfg | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 setup.cfg diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index e3456633..00000000 --- a/setup.cfg +++ /dev/null @@ -1,9 +0,0 @@ -[isort] -profile = black - -[flake8] -max-line-length = 88 -exclude = */migrations/* -ignore = E203,W503 -per-file-ignores = - __init__.py:F401,F403 From bbd61795c3fe7af23056f7d0f03ddbb0bf39f6b9 Mon Sep 17 00:00:00 2001 From: "eduard.panasicov" Date: Mon, 14 Sep 2026 15:26:11 +0300 Subject: [PATCH 08/12] OCTO-11566 Run code checks on publish --- .github/workflows/release.yml | 3 +++ .github/workflows/release_test_pypi.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 286a08f3..3cf4440b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,6 +17,9 @@ jobs: steps: - uses: actions/checkout@v7 + - name: Code checks + run: ./scripts/code_checks/run_code_checks.sh --show-diff-on-failure + - name: Set up Python uses: actions/setup-python@v6 with: diff --git a/.github/workflows/release_test_pypi.yml b/.github/workflows/release_test_pypi.yml index 9caae4c6..db3ac2a3 100644 --- a/.github/workflows/release_test_pypi.yml +++ b/.github/workflows/release_test_pypi.yml @@ -17,6 +17,9 @@ jobs: steps: - uses: actions/checkout@v7 + - name: Code checks + run: ./scripts/code_checks/run_code_checks.sh --show-diff-on-failure + - name: Set up Python uses: actions/setup-python@v6 with: From eeacd224cba23cde07bb2e7e4d9b1d8fde6ab4cd Mon Sep 17 00:00:00 2001 From: "eduard.panasicov" Date: Mon, 14 Sep 2026 15:29:11 +0300 Subject: [PATCH 09/12] OCTO-11566 Update changelog and bump version --- docs/changelog.rst | 5 +++++ docs/conf.py | 4 ++-- setup.py | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 4d4cb774..e5699f46 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,5 +1,10 @@ Changelog --------- +2.3.10 +^^^^^^ + - Replaced the existing linting tools with Ruff + - Added a linting check to the PyPI publishing workflow + 2.3.9 ^^^^^^ - Fix ``SCCReader`` inserting phantom ``BREAK`` nodes when two independent, diff --git a/docs/conf.py b/docs/conf.py index effc8c7a..1c44f637 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -53,9 +53,9 @@ # built documents. # # The short X.Y version. -version = "2.3.9" +version = "2.3.10" # The full version, including alpha/beta/rc tags. -release = "2.3.9" +release = "2.3.10" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 25055aa5..78cfbd5a 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ setup( name="pycaption", - version="2.3.9", + version="2.3.10", description="Closed caption converter", long_description=open(README_PATH).read(), author="Joe Norton", From e95b8b9e1b2469a133da0e9945d8b95370624ec5 Mon Sep 17 00:00:00 2001 From: "eduard.panasicov" Date: Mon, 14 Sep 2026 15:30:54 +0300 Subject: [PATCH 10/12] OCTO-11566 Set dev version --- docs/conf.py | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 1c44f637..ef15da24 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -53,9 +53,9 @@ # built documents. # # The short X.Y version. -version = "2.3.10" +version = "2.3.10.dev1" # The full version, including alpha/beta/rc tags. -release = "2.3.10" +release = "2.3.10.dev1" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 78cfbd5a..3f1508d9 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ setup( name="pycaption", - version="2.3.10", + version="2.3.10.dev1", description="Closed caption converter", long_description=open(README_PATH).read(), author="Joe Norton", From f413c6f3cc43268d31fd79ba56a6d0b381b10067 Mon Sep 17 00:00:00 2001 From: "eduard.panasicov" Date: Tue, 15 Sep 2026 14:03:28 +0300 Subject: [PATCH 11/12] OCTO-11566 Bump dev version --- docs/changelog.rst | 2 +- docs/conf.py | 4 ++-- setup.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index e5699f46..2dddceb3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,6 @@ Changelog --------- -2.3.10 +2.3.11 ^^^^^^ - Replaced the existing linting tools with Ruff - Added a linting check to the PyPI publishing workflow diff --git a/docs/conf.py b/docs/conf.py index ef15da24..c64e6a03 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -53,9 +53,9 @@ # built documents. # # The short X.Y version. -version = "2.3.10.dev1" +version = "2.3.11.dev1" # The full version, including alpha/beta/rc tags. -release = "2.3.10.dev1" +release = "2.3.11.dev1" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 3f1508d9..9aaf7c29 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ setup( name="pycaption", - version="2.3.10.dev1", + version="2.3.11.dev1", description="Closed caption converter", long_description=open(README_PATH).read(), author="Joe Norton", From be9b5adff3eb758d682e870fa522eb6d2f8b590d Mon Sep 17 00:00:00 2001 From: "eduard.panasicov" Date: Tue, 15 Sep 2026 14:43:54 +0300 Subject: [PATCH 12/12] OCTO-11566 Ruff auto fix --- pycaption/webvtt/writer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pycaption/webvtt/writer.py b/pycaption/webvtt/writer.py index bfb309ab..45745a7a 100644 --- a/pycaption/webvtt/writer.py +++ b/pycaption/webvtt/writer.py @@ -8,6 +8,7 @@ """ import re + from copy import deepcopy from datetime import timedelta