From 97dd0db1dfa61d65b82c5c47742f767b9496bf5d Mon Sep 17 00:00:00 2001 From: WyattBlue Date: Wed, 10 Jun 2026 23:26:44 -0400 Subject: [PATCH] Require Python 3.11 and modernize syntax --- .github/workflows/smoke.yml | 2 +- .github/workflows/tests.yml | 3 +-- av/audio/codeccontext.pyi | 3 ++- av/audio/fifo.py | 4 ++-- av/audio/frame.pyi | 16 ++++++++-------- av/codec/codec.py | 16 ++++------------ av/codec/codec.pyi | 3 ++- av/container/core.pyi | 7 ++++--- av/container/input.py | 3 +-- av/container/input.pyi | 4 ++-- av/container/output.py | 2 +- av/container/output.pyi | 4 ++-- av/container/streams.py | 2 +- av/container/streams.pyi | 3 ++- av/datasets.py | 2 +- av/dictionary.pyi | 2 +- av/filter/graph.py | 2 +- av/index.pyi | 3 ++- av/logging.py | 2 +- av/logging.pyi | 3 ++- av/packet.py | 3 ++- av/packet.pyi | 3 ++- av/sidedata/sidedata.pyi | 4 ++-- av/subtitles/subtitle.py | 2 +- av/subtitles/subtitle.pyi | 3 ++- av/video/codeccontext.pyi | 3 ++- av/video/format.py | 2 +- av/video/frame.pyi | 14 +++++++------- av/video/stream.pyi | 4 ++-- examples/basics/parse.py | 2 +- examples/basics/thread_type.py | 4 ++-- pyproject.toml | 3 +-- setup.py | 2 +- tests/common.py | 9 +++------ tests/test_codec_context.py | 11 ++++------- tests/test_subtitles.py | 4 ++-- 36 files changed, 76 insertions(+), 83 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 6fb33ad02..b4962872f 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -36,7 +36,7 @@ jobs: matrix: config: - {os: ubuntu-24.04, python: "3.12", ffmpeg: "8.0.1", extras: true} - - {os: ubuntu-24.04, python: "3.10", ffmpeg: "8.0.1"} + - {os: ubuntu-24.04, python: "3.11", ffmpeg: "8.0.1"} - {os: ubuntu-24.04, python: "3.13", ffmpeg: "8.1"} - {os: macos-14, python: "3.11", ffmpeg: "8.0.1"} - {os: macos-14, python: "3.14", ffmpeg: "8.1"} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7098f7cf3..9f20ee93f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -70,8 +70,7 @@ jobs: CIBW_ENVIRONMENT_MACOS: PKG_CONFIG_PATH=/tmp/vendor/lib/pkgconfig LDFLAGS=-headerpad_max_install_names CIBW_ENVIRONMENT_WINDOWS: INCLUDE=C:\\cibw\\vendor\\include LIB=C:\\cibw\\vendor\\lib PYAV_SKIP_TESTS=unicode_filename CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: delvewheel repair --add-path C:\cibw\vendor\bin -w {dest_dir} {wheel} - CIBW_BUILD: "cp310* cp311* cp314t*" - CIBW_SKIP: "cp310-win_arm64" + CIBW_BUILD: "cp311* cp314t*" CIBW_TEST_COMMAND: mv {project}/av {project}/av.disabled && python -m pytest {package}/tests && mv {project}/av.disabled {project}/av CIBW_TEST_REQUIRES: pytest numpy CIBW_TEST_SKIP: "*_armv7l" diff --git a/av/audio/codeccontext.pyi b/av/audio/codeccontext.pyi index b3ec3ce6e..e871db698 100644 --- a/av/audio/codeccontext.pyi +++ b/av/audio/codeccontext.pyi @@ -1,4 +1,5 @@ -from typing import Iterator, Literal +from collections.abc import Iterator +from typing import Literal from av.codec.context import CodecContext from av.packet import Packet diff --git a/av/audio/fifo.py b/av/audio/fifo.py index 0fd128ee7..00db125b7 100644 --- a/av/audio/fifo.py +++ b/av/audio/fifo.py @@ -95,8 +95,8 @@ def write(self, frame: AudioFrame | None): ) if frame.ptr.pts != expected_pts: raise ValueError( - "Frame.pts (%d) != expected (%d); fix or set to None." - % (frame.ptr.pts, expected_pts) + f"Frame.pts ({frame.ptr.pts}) != expected ({expected_pts}); " + "fix or set to None." ) err_check( diff --git a/av/audio/frame.pyi b/av/audio/frame.pyi index 6aeb86b4d..8efb26760 100644 --- a/av/audio/frame.pyi +++ b/av/audio/frame.pyi @@ -1,4 +1,4 @@ -from typing import Any, Union +from typing import Any import numpy as np @@ -9,13 +9,13 @@ from .layout import AudioLayout from .plane import AudioPlane format_dtypes: dict[str, str] -_SupportedNDarray = Union[ - np.ndarray[Any, np.dtype[np.float64]], # f8 - np.ndarray[Any, np.dtype[np.float32]], # f4 - np.ndarray[Any, np.dtype[np.int32]], # i4 - np.ndarray[Any, np.dtype[np.int16]], # i2 - np.ndarray[Any, np.dtype[np.uint8]], # u1 -] +_SupportedNDarray = ( + np.ndarray[Any, np.dtype[np.float64]] # f8 + | np.ndarray[Any, np.dtype[np.float32]] # f4 + | np.ndarray[Any, np.dtype[np.int32]] # i4 + | np.ndarray[Any, np.dtype[np.int16]] # i2 + | np.ndarray[Any, np.dtype[np.uint8]] # u1 +) class _Format: def __get__(self, i: object | None, owner: type | None = None) -> AudioFormat: ... diff --git a/av/codec/codec.py b/av/codec/codec.py index aff456ecb..616980641 100644 --- a/av/codec/codec.py +++ b/av/codec/codec.py @@ -116,7 +116,7 @@ def _init(self, name=None): if not self.desc: self.desc = lib.avcodec_descriptor_get(self.ptr.id) if not self.desc: - raise RuntimeError("No codec descriptor for %r." % name) + raise RuntimeError(f"No codec descriptor for {name!r}.") self.is_encoder = lib.av_codec_is_encoder(self.ptr) @@ -384,17 +384,9 @@ def dump_codecs(): try: print( - " %s%s%s%s%s%s %-18s %s" - % ( - ".D"[bool(d_codec)], - ".E"[bool(e_codec)], - codec.type[0].upper(), - ".I"[codec.intra_only], - ".L"[codec.lossy], - ".S"[codec.lossless], - codec.name, - codec.long_name, - ) + f" {'.D'[bool(d_codec)]}{'.E'[bool(e_codec)]}{codec.type[0].upper()}" + f"{'.I'[codec.intra_only]}{'.L'[codec.lossy]}{'.S'[codec.lossless]}" + f" {codec.name:<18} {codec.long_name}" ) except Exception as e: print(f"...... {codec.name:<18} ERROR: {e}") diff --git a/av/codec/codec.pyi b/av/codec/codec.pyi index 1459bc777..32c83371e 100644 --- a/av/codec/codec.pyi +++ b/av/codec/codec.pyi @@ -1,6 +1,7 @@ +from collections.abc import Sequence from enum import Flag, IntEnum from fractions import Fraction -from typing import ClassVar, Literal, Sequence, cast, overload +from typing import ClassVar, Literal, cast, overload from av.audio.codeccontext import AudioCodecContext from av.audio.format import AudioFormat diff --git a/av/container/core.pyi b/av/container/core.pyi index 81e4936e4..ec4a84d89 100644 --- a/av/container/core.pyi +++ b/av/container/core.pyi @@ -1,8 +1,9 @@ +from collections.abc import Callable from enum import Flag, IntEnum from fractions import Fraction from pathlib import Path from types import TracebackType -from typing import Any, Callable, ClassVar, Literal, Type, TypedDict, cast, overload +from typing import Any, ClassVar, Literal, Self, TypedDict, cast, overload from av.codec.hwaccel import HWAccel from av.format import ContainerFormat @@ -92,10 +93,10 @@ class Container: read_timeout: Real | None flags: int video_codec_id: int - def __enter__(self) -> Container: ... + def __enter__(self) -> Self: ... def __exit__( self, - exc_type: Type[BaseException] | None, + exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None, ) -> None: ... diff --git a/av/container/input.py b/av/container/input.py index f5fc52547..4881e720d 100644 --- a/av/container/input.py +++ b/av/container/input.py @@ -238,8 +238,7 @@ def decode(self, *args, **kwargs): """ self._assert_open() for packet in self.demux(*args, **kwargs): - for frame in packet.decode(): - yield frame + yield from packet.decode() def seek( self, diff --git a/av/container/input.pyi b/av/container/input.pyi index a5f829fef..88dd07d69 100644 --- a/av/container/input.pyi +++ b/av/container/input.pyi @@ -1,4 +1,5 @@ -from typing import Any, Iterator, overload +from collections.abc import Iterator +from typing import Any, overload from av.audio.frame import AudioFrame from av.audio.stream import AudioStream @@ -17,7 +18,6 @@ class InputContainer(Container): bit_rate: int size: int - def __enter__(self) -> InputContainer: ... @overload def demux(self, video_stream: VideoStream) -> Iterator[Packet[VideoStream]]: ... @overload diff --git a/av/container/output.py b/av/container/output.py index 239e550f2..1b8f846e9 100644 --- a/av/container/output.py +++ b/av/container/output.py @@ -496,7 +496,7 @@ def start_encoding(self): import logging log = logging.getLogger(__name__) - log.warning("Some options were not used: %s" % unused_options) + log.warning(f"Some options were not used: {unused_options}") self._myflag |= 4 diff --git a/av/container/output.pyi b/av/container/output.pyi index f7781be29..ea42b1439 100644 --- a/av/container/output.pyi +++ b/av/container/output.pyi @@ -1,5 +1,6 @@ +from collections.abc import Sequence from fractions import Fraction -from typing import Sequence, TypeVar, overload +from typing import TypeVar, overload from av.audio import _AudioCodecName from av.audio.stream import AudioStream @@ -14,7 +15,6 @@ from .core import Container _StreamT = TypeVar("_StreamT", bound=Stream) class OutputContainer(Container): - def __enter__(self) -> OutputContainer: ... @overload def add_stream( self, diff --git a/av/container/streams.py b/av/container/streams.py index e588694b4..3b1dca00b 100644 --- a/av/container/streams.py +++ b/av/container/streams.py @@ -1,4 +1,4 @@ -from typing import Iterator +from collections.abc import Iterator import cython import cython.cimports.libav as lib diff --git a/av/container/streams.pyi b/av/container/streams.pyi index 9e0df35a4..bdab1ea54 100644 --- a/av/container/streams.pyi +++ b/av/container/streams.pyi @@ -1,4 +1,5 @@ -from typing import Iterator, Literal, overload +from collections.abc import Iterator +from typing import Literal, overload from av.audio.stream import AudioStream from av.stream import AttachmentStream, DataStream, Stream diff --git a/av/datasets.py b/av/datasets.py index 5954a9c98..237b3bf04 100644 --- a/av/datasets.py +++ b/av/datasets.py @@ -2,7 +2,7 @@ import logging import os import sys -from typing import Iterator +from collections.abc import Iterator from urllib.request import urlopen log = logging.getLogger(__name__) diff --git a/av/dictionary.pyi b/av/dictionary.pyi index 7994c1306..f68003c6a 100644 --- a/av/dictionary.pyi +++ b/av/dictionary.pyi @@ -1,4 +1,4 @@ -from typing import Iterable, Iterator, Mapping +from collections.abc import Iterable, Iterator, Mapping class Dictionary: def __getitem__(self, key: str) -> str: ... diff --git a/av/filter/graph.py b/av/filter/graph.py index c1b61eb20..a7648f967 100644 --- a/av/filter/graph.py +++ b/av/filter/graph.py @@ -50,7 +50,7 @@ def _get_unique_name(self, name: str) -> str: count = self._name_counts.get(name, 0) self._name_counts[name] = count + 1 if count: - return "%s_%s" % (name, count) + return f"{name}_{count}" else: return name diff --git a/av/index.pyi b/av/index.pyi index b74262609..1ccec7a66 100644 --- a/av/index.pyi +++ b/av/index.pyi @@ -1,4 +1,5 @@ -from typing import Iterator, overload +from collections.abc import Iterator +from typing import overload class IndexEntry: pos: int diff --git a/av/logging.py b/av/logging.py index 295779444..1d484b679 100644 --- a/av/logging.py +++ b/av/logging.py @@ -288,7 +288,7 @@ def log_callback_gil( repeat_log = ( last_log[0], last_log[1], - "%s (repeated %d more times)" % (last_log[2], skip_count), + f"{last_log[2]} (repeated {skip_count} more times)", ) skip_count = 0 diff --git a/av/logging.pyi b/av/logging.pyi index 8c32de77d..6193ad7b8 100644 --- a/av/logging.pyi +++ b/av/logging.pyi @@ -1,4 +1,5 @@ -from typing import Any, Callable +from collections.abc import Callable +from typing import Any PANIC: int FATAL: int diff --git a/av/packet.py b/av/packet.py index a69cf38c1..7886b37ad 100644 --- a/av/packet.py +++ b/av/packet.py @@ -1,4 +1,5 @@ -from typing import Iterator, Literal, get_args +from collections.abc import Iterator +from typing import Literal, get_args import cython from cython.cimports import libav as lib diff --git a/av/packet.pyi b/av/packet.pyi index 3a0af39ef..805a3cf97 100644 --- a/av/packet.pyi +++ b/av/packet.pyi @@ -1,5 +1,6 @@ +from collections.abc import Iterator from fractions import Fraction -from typing import Generic, Iterator, Literal, TypeVar, overload +from typing import Generic, Literal, TypeVar, overload from av.audio.frame import AudioFrame from av.audio.stream import AudioStream diff --git a/av/sidedata/sidedata.pyi b/av/sidedata/sidedata.pyi index 0093fabd0..3f8ef398e 100644 --- a/av/sidedata/sidedata.pyi +++ b/av/sidedata/sidedata.pyi @@ -1,6 +1,6 @@ -from collections.abc import Mapping +from collections.abc import Iterator, Mapping, Sequence from enum import Enum -from typing import ClassVar, Iterator, Sequence, cast, overload +from typing import ClassVar, cast, overload from av.buffer import Buffer from av.frame import Frame diff --git a/av/subtitles/subtitle.py b/av/subtitles/subtitle.py index 5c58647a9..2d2b53df6 100644 --- a/av/subtitles/subtitle.py +++ b/av/subtitles/subtitle.py @@ -172,7 +172,7 @@ def build_subtitle(subtitle: SubtitleSet, index: cython.int) -> Subtitle: if ptr.type == lib.SUBTITLE_ASS or ptr.type == lib.SUBTITLE_TEXT: return AssSubtitle(subtitle, index) - raise ValueError("unknown subtitle type %r" % ptr.type) + raise ValueError(f"unknown subtitle type {ptr.type!r}") @cython.cclass diff --git a/av/subtitles/subtitle.pyi b/av/subtitles/subtitle.pyi index d3d4201fd..5f277eba0 100644 --- a/av/subtitles/subtitle.pyi +++ b/av/subtitles/subtitle.pyi @@ -1,4 +1,5 @@ -from typing import Iterator, Literal +from collections.abc import Iterator +from typing import Literal class SubtitleSet: format: int diff --git a/av/video/codeccontext.pyi b/av/video/codeccontext.pyi index 14c520beb..280f5cdc0 100644 --- a/av/video/codeccontext.pyi +++ b/av/video/codeccontext.pyi @@ -1,5 +1,6 @@ +from collections.abc import Iterator from fractions import Fraction -from typing import Iterator, Literal +from typing import Literal from av.codec.context import CodecContext from av.packet import Packet diff --git a/av/video/format.py b/av/video/format.py index ce2246836..654f0f7fe 100644 --- a/av/video/format.py +++ b/av/video/format.py @@ -23,7 +23,7 @@ def get_pix_fmt(name: cython.p_const_char) -> lib.AVPixelFormat: pix_fmt: lib.AVPixelFormat = lib.av_get_pix_fmt(name) if pix_fmt == lib.AV_PIX_FMT_NONE: - raise ValueError("not a pixel format: %r" % name) + raise ValueError(f"not a pixel format: {name!r}") return pix_fmt diff --git a/av/video/frame.pyi b/av/video/frame.pyi index 12a85182b..9e98a7033 100644 --- a/av/video/frame.pyi +++ b/av/video/frame.pyi @@ -1,6 +1,6 @@ from enum import IntEnum from pathlib import Path -from typing import Any, Union +from typing import Any import numpy as np @@ -10,12 +10,12 @@ from .format import VideoFormat from .plane import VideoPlane from .reformatter import ColorPrimaries, ColorTrc -_SupportedNDarray = Union[ - np.ndarray[Any, np.dtype[np.uint8]], - np.ndarray[Any, np.dtype[np.uint16]], - np.ndarray[Any, np.dtype[np.float16]], - np.ndarray[Any, np.dtype[np.float32]], -] +_SupportedNDarray = ( + np.ndarray[Any, np.dtype[np.uint8]] + | np.ndarray[Any, np.dtype[np.uint16]] + | np.ndarray[Any, np.dtype[np.float16]] + | np.ndarray[Any, np.dtype[np.float32]] +) supported_np_pix_fmts: set[str] diff --git a/av/video/stream.pyi b/av/video/stream.pyi index 4e2a61e46..e6797f413 100644 --- a/av/video/stream.pyi +++ b/av/video/stream.pyi @@ -1,6 +1,6 @@ -from collections.abc import Sequence +from collections.abc import Iterator, Sequence from fractions import Fraction -from typing import Iterator, Literal +from typing import Literal from av.codec.context import ThreadType from av.packet import Packet diff --git a/examples/basics/parse.py b/examples/basics/parse.py index f4baaecb7..5d052449f 100644 --- a/examples/basics/parse.py +++ b/examples/basics/parse.py @@ -31,7 +31,7 @@ chunk = fh.read(1 << 16) packets = codec.parse(chunk) - print("Parsed {} packets from {} bytes:".format(len(packets), len(chunk))) + print(f"Parsed {len(packets)} packets from {len(chunk)} bytes:") for packet in packets: print(" ", packet) diff --git a/examples/basics/thread_type.py b/examples/basics/thread_type.py index 966a8c8c0..fab539b26 100644 --- a/examples/basics/thread_type.py +++ b/examples/basics/thread_type.py @@ -38,5 +38,5 @@ container.close() -print("Decoded with default threading in {:.2f}s.".format(default_time)) -print("Decoded with auto threading in {:.2f}s.".format(auto_time)) +print(f"Decoded with default threading in {default_time:.2f}s.") +print(f"Decoded with auto threading in {auto_time:.2f}s.") diff --git a/pyproject.toml b/pyproject.toml index ce542dc46..54fb5c736 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ authors = [ {name = "WyattBlue", email = "wyattblue@auto-editor.com"}, {name = "Jeremy Lainé", email = "jeremy.laine@m4x.org"}, ] -requires-python = ">=3.10" +requires-python = ">=3.11" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", @@ -20,7 +20,6 @@ classifiers = [ "Operating System :: Unix", "Operating System :: Microsoft :: Windows", "Programming Language :: Cython", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", diff --git a/setup.py b/setup.py index 3a307c4c6..4d3475cfe 100644 --- a/setup.py +++ b/setup.py @@ -94,7 +94,7 @@ def get_config_from_pkg_config(): known, unknown = parse_cflags(raw_cflags.decode("utf-8")) if unknown: - print("pkg-config returned flags we don't understand: {}".format(unknown)) + print(f"pkg-config returned flags we don't understand: {unknown}") if "-pthread" in unknown: print("Building PyAV against static FFmpeg libraries is not supported.") exit(1) diff --git a/tests/common.py b/tests/common.py index d0cdac2e0..19984c77c 100644 --- a/tests/common.py +++ b/tests/common.py @@ -20,7 +20,8 @@ has_pillow = False if TYPE_CHECKING: - from typing import Any, Callable, TypeVar + from collections.abc import Callable + from typing import Any, TypeVar from PIL.Image import Image @@ -105,11 +106,7 @@ def assertNdarraysEqual(a: np.ndarray, b: np.ndarray) -> None: msg = "" for equal in it: if not equal: - msg += "- arrays differ at index {}; {} {}\n".format( - it.multi_index, - a[it.multi_index], - b[it.multi_index], - ) + msg += f"- arrays differ at index {it.multi_index}; {a[it.multi_index]} {b[it.multi_index]}\n" assert False, f"ndarrays contents differ\n{msg}" diff --git a/tests/test_codec_context.py b/tests/test_codec_context.py index 1f9dc2ee0..dcd212b9e 100644 --- a/tests/test_codec_context.py +++ b/tests/test_codec_context.py @@ -1,8 +1,9 @@ from __future__ import annotations import os +from collections.abc import Iterator from fractions import Fraction -from typing import Iterator, TypedDict, overload +from typing import TypedDict, overload import pytest @@ -273,12 +274,8 @@ def image_sequence_encode(self, codec_name: str) -> None: new_packet = new_packets[0] path = self.sandboxed( - "%s/encoder.%04d.%s" - % ( - codec_name, - frame_count, - codec_name if codec_name != "mjpeg" else "jpg", - ) + f"{codec_name}/encoder.{frame_count:04d}." + f"{codec_name if codec_name != 'mjpeg' else 'jpg'}" ) path_list.append(path) with open(path, "wb") as f: diff --git a/tests/test_subtitles.py b/tests/test_subtitles.py index dd13d7756..c3d462b59 100644 --- a/tests/test_subtitles.py +++ b/tests/test_subtitles.py @@ -123,11 +123,11 @@ def test_subtitle_dialogue_extended_chars(self) -> None: """Test handling of extended UTF-8 characters in subtitle dialogue.""" from av.subtitles.subtitle import SubtitleSet - text = "0,0,Default,,0,0,0,,♪ Hey, hey, hey ♪".encode("utf-8") + text = "0,0,Default,,0,0,0,,♪ Hey, hey, hey ♪".encode() subtitle = SubtitleSet.create(text=text, start=0, end=2000, pts=0) sub = cast(AssSubtitle, subtitle[0]) - assert sub.dialogue == "♪ Hey, hey, hey ♪".encode("utf-8") + assert sub.dialogue == "♪ Hey, hey, hey ♪".encode() def test_subtitle_encode_mp4(self) -> None: """Test encoding subtitles to MP4 container."""