From 4794e47f461c743b7b68da1a9ffb883ec5ce4a5c Mon Sep 17 00:00:00 2001 From: Hugues Hoppe Date: Sun, 26 Apr 2026 21:36:59 -0700 Subject: [PATCH 01/14] Fixes for mypy and pylint --- mediapy/__init__.py | 12 +++++++----- pdoc_files/make.py | 1 + 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/mediapy/__init__.py b/mediapy/__init__.py index a1b505e..142ae53 100644 --- a/mediapy/__init__.py +++ b/mediapy/__init__.py @@ -765,11 +765,11 @@ def __exit__(self, *_: Any) -> None: # ** Image I/O. -def read_image( +def read_image( # pyrefly: ignore[bad-function-definition] path_or_url: _Path, *, apply_exif_transpose: bool = True, - dtype: _DTypeLike = None, # pyrefly: ignore[bad-function-definition] + dtype: _DTypeLike | None = None, ) -> _NDArray: """Returns an image read from a file path or URL. @@ -840,7 +840,7 @@ def to_rgb( # vmax = np.amax(a, where=np.isfinite(a)) if vmax is None else vmax vmin = np.amin(np.where(np.isfinite(a), a, np.inf)) if vmin is None else vmin vmax = np.amax(np.where(np.isfinite(a), a, -np.inf)) if vmax is None else vmax - a = (a.astype('float') - vmin) / (vmax - vmin + np.finfo(float).eps) + a = (a.astype('float') - vmin) / (vmax - vmin + np.finfo(float).eps) # pylint: disable=no-member if isinstance(cmap, str): if hasattr(matplotlib, 'colormaps'): rgb_from_scalar: Any = matplotlib.colormaps[cmap] # Newer version. @@ -872,8 +872,10 @@ def compress_image( return output.getvalue() -def decompress_image( - data: bytes, dtype: _DTypeLike = None, apply_exif_transpose: bool = True # pyrefly: ignore[bad-function-definition] +def decompress_image( # pyrefly: ignore[bad-function-definition] + data: bytes, + dtype: _DTypeLike | None = None, + apply_exif_transpose: bool = True, ) -> _NDArray: """Returns an image from a compressed data buffer. diff --git a/pdoc_files/make.py b/pdoc_files/make.py index 8a65ac4..b180e32 100644 --- a/pdoc_files/make.py +++ b/pdoc_files/make.py @@ -13,6 +13,7 @@ # limitations under the License. """Create HTML documentation from the source code using `pdoc`.""" + # Note: Invoke this from the parent directory as "python3 pdoc_files/make.py". import pathlib From ab68e5fa0c61da8c78b9703ceffaadc471eacc16 Mon Sep 17 00:00:00 2001 From: Hugues Hoppe Date: Thu, 23 Jul 2026 10:56:50 -0700 Subject: [PATCH 02/14] mypy fix --- mediapy/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mediapy/__init__.py b/mediapy/__init__.py index 142ae53..e193444 100644 --- a/mediapy/__init__.py +++ b/mediapy/__init__.py @@ -845,7 +845,8 @@ def to_rgb( if hasattr(matplotlib, 'colormaps'): rgb_from_scalar: Any = matplotlib.colormaps[cmap] # Newer version. else: - rgb_from_scalar = matplotlib.pyplot.cm.get_cmap(cmap) # pylint: disable=no-member + # pylint: disable-next=no-member + rgb_from_scalar = matplotlib.pyplot.cm.get_cmap(cmap) # type: ignore[attr-defined, unused-ignore] else: rgb_from_scalar = cmap a = typing.cast(_NDArray, rgb_from_scalar(a)) @@ -1245,7 +1246,7 @@ def _run_ffmpeg( env: Any = None # pylint: disable=unused-variable ffmpeg_path = _get_ffmpeg_path() - # Sandbox max runtime, allowed input and ouput files are not supported in + # Sandbox max runtime, allowed input and output files are not supported in # open source. del allowed_input_files del allowed_output_files From 6f6ac0cd2d7850aef61c5c2919de013126dafbb0 Mon Sep 17 00:00:00 2001 From: Hugues Hoppe Date: Fri, 24 Jul 2026 19:02:26 -0700 Subject: [PATCH 03/14] Clean up tests to eliminate pyrefly pragmas --- mediapy_test.py | 211 ++++++++++++++++++++++-------------------------- 1 file changed, 98 insertions(+), 113 deletions(-) diff --git a/mediapy_test.py b/mediapy_test.py index f33c897..513d8a4 100755 --- a/mediapy_test.py +++ b/mediapy_test.py @@ -21,12 +21,12 @@ import tempfile from unittest import mock -from absl.testing import absltest -from absl.testing import parameterized -import IPython -import matplotlib -import mediapy as media +import IPython.display +import matplotlib.pyplot import numpy as np +from absl.testing import absltest, parameterized + +import mediapy as media # pylint: disable=missing-function-docstring, protected-access # pylint: disable=too-many-public-methods @@ -424,7 +424,7 @@ def gray(x): cmap = matplotlib.colormaps['gray'] # Newer version. else: cmap = matplotlib.pyplot.cm.get_cmap('gray') # pylint: disable=no-member - return cmap(x)[..., :3] + return cmap(x)[..., :3] # pyrefly: ignore[bad-index] self.assert_all_close(media.to_rgb(a), gray([0.0, 0.5, 1.0])) self.assert_all_close( @@ -449,15 +449,20 @@ def test_compress_decompress_image_roundtrip(self, dtype): self.assertEqual(image.dtype, new_image.dtype) self.assert_all_equal(image, new_image) - def test_show_image(self): + def capture_html_string(self, func) -> str: htmls = [] with mock.patch('IPython.display.display', htmls.append): - media.show_image(media.color_ramp()) + func() self.assertLen(htmls, 1) self.assertIsInstance(htmls[0], IPython.display.HTML) - self.assertLen(re.findall('(?s)]*/>') # pyrefly: ignore[bad-specialization] - self.assertLen(re.findall('(?s)]*/>') + self.assertLen(re.findall('(?s)', htmls[0].data), 1) # pyrefly: ignore[no-matching-overload] - self.assertLen(re.findall('(?s)base64', htmls[0].data), 2) # pyrefly: ignore[no-matching-overload] - self.assertEmpty(re.findall('(?s)b64', htmls[0].data)) # pyrefly: ignore[no-matching-overload] + s = self.capture_html_string( + lambda: media.compare_images([media.color_ramp()] * 2) + ) + self.assertLen(re.findall('(?s)', s), 1) + self.assertLen(re.findall('(?s)base64', s), 2) + self.assertEmpty(re.findall('(?s)b64', s)) @parameterized.parameters([False, True], [False, True]) def test_video_non_streaming_write_read_roundtrip( @@ -573,7 +566,8 @@ def test_video_non_streaming_write_read_roundtrip( self.assertEqual(new_video.metadata.num_images, num_images) self.assertEqual(new_video.metadata.shape, shape) self.assertEqual(new_video.metadata.fps, fps) - self.assertGreater(new_video.metadata.bps, 1_000) # pyrefly: ignore[no-matching-overload] + self.assertIsInstance(new_video.metadata.bps, (int, float)) + self.assertGreater(new_video.metadata.bps, 1_000) self._check_similar(original_video, new_video, max_rms) def test_video_streaming_write_read_roundtrip(self): @@ -629,7 +623,8 @@ def test_video_streaming_read_write(self): self.assertEqual(new_video.metadata.num_images, num_images) self.assertEqual(new_video.metadata.shape, shape) self.assertEqual(new_video.metadata.fps, fps) - self.assertGreater(new_video.metadata.bps, 1_000) # pyrefly: ignore[no-matching-overload] + self.assertIsInstance(new_video.metadata.bps, (int, float)) + self.assertGreater(new_video.metadata.bps, 1_000) self._check_similar(video, new_video, 3.0) def test_video_read_write_10bit(self): @@ -698,21 +693,17 @@ def test_html_from_compressed_video(self): ) def test_show_video(self): - htmls = [] - with mock.patch('IPython.display.display', htmls.append): - media.show_video(media.moving_circle()) - self.assertLen(htmls, 1) - self.assertIsInstance(htmls[0], IPython.display.HTML) - self.assertLen(re.findall('(?s).*') # pyrefly: ignore[bad-specialization] + s = self.capture_html_string( + lambda: media.show_video(media.moving_circle()) + ) + self.assertLen(re.findall('(?s).*') def test_show_video_gif(self): - htmls = [] - with mock.patch('IPython.display.display', htmls.append): - media.show_video(media.moving_circle(), codec='gif') - self.assertLen(htmls, 1) - self.assertIsInstance(htmls[0], IPython.display.HTML) - self.assertContainsInOrder([' Date: Fri, 24 Jul 2026 19:07:21 -0700 Subject: [PATCH 04/14] Set up pyrefly and ruff in pyproject.toml and clean up the code to avoid warnings --- mediapy/__init__.py | 129 ++++++++++++++++++++++---------------------- pyproject.toml | 20 +++++++ 2 files changed, 84 insertions(+), 65 deletions(-) diff --git a/mediapy/__init__.py b/mediapy/__init__.py index e193444..13e995b 100644 --- a/mediapy/__init__.py +++ b/mediapy/__init__.py @@ -108,7 +108,6 @@ __version_info__ = tuple(int(num) for num in __version__.split('.')) import base64 -from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence import contextlib import functools import importlib @@ -125,9 +124,10 @@ import sys import tempfile import typing -from typing import Any import urllib.request import warnings +from collections.abc import Callable, Generator, Iterable, Iterator, Mapping, Sequence +from typing import Any import IPython.display import matplotlib.pyplot @@ -136,12 +136,11 @@ import PIL.Image import PIL.ImageOps - if not hasattr(PIL.Image, 'Resampling'): # Allow Pillow<9.0. PIL.Image.Resampling = PIL.Image # type: ignore # Selected and reordered here for pdoc documentation. -__all__ = [ +__all__ = [ # noqa: RUF022 'show_image', 'show_images', 'compare_images', @@ -220,7 +219,7 @@ class _Config: def _open(path: _Path, *args: Any, **kwargs: Any) -> Any: """Opens the file; this is a hook for the built-in `open()`.""" - return open(path, *args, **kwargs) + return open(path, *args, **kwargs) # noqa: PTH123 def _path_is_local(path: _Path) -> bool: @@ -232,7 +231,7 @@ def _path_is_local(path: _Path) -> bool: def _search_for_ffmpeg_path() -> str | None: """Returns a path to the ffmpeg program, or None if not found.""" if filename := shutil.which(_config.ffmpeg_name_or_path): - return str(filename) + return str(pathlib.Path(filename)) return None @@ -297,7 +296,7 @@ def _run(args: str | Sequence[str]) -> None: stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=False, - universal_newlines=True, + text=True, ) print(proc.stdout, end='', flush=True) if proc.returncode: @@ -327,18 +326,16 @@ def set_ffmpeg(name_or_path: _Path) -> None: def set_output_height(num_pixels: int) -> None: """Overrides the height of the current output cell, if using Colab.""" - try: + with contextlib.suppress(ModuleNotFoundError, AttributeError): # We want to fail gracefully for non-Colab IPython notebooks. output = importlib.import_module('google.colab.output') s = f'google.colab.output.setIframeHeight("{num_pixels}px")' output.eval_js(s) - except (ModuleNotFoundError, AttributeError): - pass def set_max_output_height(num_pixels: int) -> None: """Sets the maximum height of the current output cell, if using Colab.""" - try: + with contextlib.suppress(ModuleNotFoundError, AttributeError): # We want to fail gracefully for non-Colab IPython notebooks. output = importlib.import_module('google.colab.output') s = ( @@ -346,8 +343,6 @@ def set_max_output_height(num_pixels: int) -> None: f'0, true, {{maxHeight: {num_pixels}}})' ) output.eval_js(s) - except (ModuleNotFoundError, AttributeError): - pass # ** Type conversions. @@ -393,42 +388,43 @@ def to_type(array: _ArrayLike, dtype: _DTypeLike) -> _NDArray: Array `a` if it is already of the specified dtype, else a converted array. """ a = np.asarray(array) - dtype = np.dtype(dtype) - del array + dtype2: np.dtype[Any] = np.dtype(dtype) + del array, dtype if a.dtype != bool: _as_valid_media_type(a.dtype) # Verify that 'a' has a valid dtype. if a.dtype == bool: - result = a.astype(dtype) - if np.issubdtype(dtype, np.unsignedinteger): - result = result * dtype.type(np.iinfo(dtype).max) # pyrefly: ignore[no-matching-overload] - elif a.dtype == dtype: + result = a.astype(dtype2) + if np.issubdtype(dtype2, np.unsignedinteger): + # Force in-place multiplication to preserve the exact dtype. + np.multiply(result, np.iinfo(dtype2).max, out=result) + elif a.dtype == dtype2: result = a - elif np.issubdtype(dtype, np.unsignedinteger): + elif np.issubdtype(dtype2, np.unsignedinteger): if np.issubdtype(a.dtype, np.unsignedinteger): src_max: float = np.iinfo(a.dtype).max else: a = np.clip(a, 0.0, 1.0) src_max = 1.0 - dst_max = np.iinfo(dtype).max # pyrefly: ignore[no-matching-overload] + dst_max = np.iinfo(dtype2).max if dst_max <= np.iinfo(np.uint16).max: scale = np.array(dst_max / src_max, dtype=np.float32) - result = (a * scale + 0.5).astype(dtype) + result = (a * scale + 0.5).astype(dtype2) elif dst_max <= np.iinfo(np.uint32).max: - result = (a.astype(np.float64) * (dst_max / src_max) + 0.5).astype(dtype) + result = (a.astype(np.float64) * (dst_max / src_max) + 0.5).astype(dtype2) else: # https://stackoverflow.com/a/66306123/ a = a.astype(np.float64) * (dst_max / src_max) + 0.5 dst = np.atleast_1d(a) values_too_large = dst >= np.float64(dst_max) with np.errstate(invalid='ignore'): - dst = dst.astype(dtype) + dst = dst.astype(dtype2) dst[values_too_large] = dst_max result = dst if a.ndim > 0 else dst[0] else: - assert np.issubdtype(dtype, np.floating) - result = a.astype(dtype) + assert np.issubdtype(dtype2, np.floating) + result = a.astype(dtype2) if np.issubdtype(a.dtype, np.unsignedinteger): - result = result / dtype.type(np.iinfo(a.dtype).max) + result = result / dtype2.type(np.iinfo(a.dtype).max) return result @@ -635,10 +631,9 @@ def resize_image(image: _ArrayLike, shape: tuple[int, int]) -> _NDArray: image.dtype == np.uint8 and image.ndim == 3 and image.shape[2] in (3, 4) ) if supported_single_channel or supported_multichannel: + h, w = shape return np.array( - _pil_image(image).resize( - shape[::-1], resample=PIL.Image.Resampling.LANCZOS - ), + _pil_image(image).resize((w, h), resample=PIL.Image.Resampling.LANCZOS), dtype=image.dtype, ) if image.ndim == 2: @@ -692,7 +687,7 @@ def read_contents(path_or_url: _Path) -> bytes: @contextlib.contextmanager -def _read_via_local_file(path_or_url: _Path) -> Iterator[str]: +def _read_via_local_file(path_or_url: _Path) -> Generator[str, None, None]: """Context to copy a remote file locally to read from it. Args: @@ -712,7 +707,7 @@ def _read_via_local_file(path_or_url: _Path) -> Iterator[str]: @contextlib.contextmanager -def _write_via_local_file(path: _Path) -> Iterator[str]: +def _write_via_local_file(path: _Path) -> Generator[str, None, None]: """Context to write a temporary local file and subsequently copy it remotely. Args: @@ -765,7 +760,7 @@ def __exit__(self, *_: Any) -> None: # ** Image I/O. -def read_image( # pyrefly: ignore[bad-function-definition] +def read_image( path_or_url: _Path, *, apply_exif_transpose: bool = True, @@ -873,7 +868,7 @@ def compress_image( return output.getvalue() -def decompress_image( # pyrefly: ignore[bad-function-definition] +def decompress_image( data: bytes, dtype: _DTypeLike | None = None, apply_exif_transpose: bool = True, @@ -891,9 +886,7 @@ def decompress_image( # pyrefly: ignore[bad-function-definition] """ pil_image: PIL.Image.Image = PIL.Image.open(io.BytesIO(data)) if apply_exif_transpose: - tmp_image = PIL.ImageOps.exif_transpose(pil_image) # Future: in_place=True. - assert tmp_image - pil_image = tmp_image + pil_image = PIL.ImageOps.exif_transpose(pil_image) # Future: in_place=True. if dtype is None: dtype = np.uint16 if pil_image.mode.startswith('I') else np.uint8 return np.array(pil_image, dtype=dtype) @@ -952,7 +945,7 @@ def _get_width_height( return width, int(width * (shape[0] / shape[1]) + 0.5) if height and not width: return int(height * (shape[1] / shape[0]) + 0.5), height - return shape[::-1] # pyrefly: ignore[bad-return] + return shape[1], shape[0] def _ensure_mapped_to_rgb( @@ -1054,23 +1047,26 @@ def show_images( Returns: html string if `return_html` is `True`. """ + list_titles: list[str | None] if isinstance(images, Mapping): if titles is not None: raise ValueError('Cannot have images dictionary and titles parameter.') - list_titles, list_images = list(images.keys()), list(images.values()) + list_images0 = list(images.values()) + list_titles = typing.cast(list[str | None], list(images.keys())) else: - list_images = list(images) - list_titles = [None] * len(list_images) if titles is None else list(titles) - if len(list_images) != len(list_titles): + list_images0 = list(images) + list_titles = [None] * len(list_images0) if titles is None else list(titles) + if len(list_images0) != len(list_titles): raise ValueError( 'Number of images does not match number of titles' - f' ({len(list_images)} vs {len(list_titles)}).' + f' ({len(list_images0)} vs {len(list_titles)}).' ) list_images = [ _ensure_mapped_to_rgb(image, vmin=vmin, vmax=vmax, cmap=cmap) - for image in list_images + for image in list_images0 ] + del list_images0 def maybe_downsample(image: _NDArray) -> _NDArray: shape = image.shape[0], image.shape[1] @@ -1092,12 +1088,13 @@ def maybe_downsample(image: _NDArray) -> _NDArray: def html_from_compressed_images() -> str: html_strings = [] for image, title, png_data in zip(list_images, list_titles, png_datas): - w, h = _get_width_height(width, height, image.shape[:2]) # pyrefly: ignore[missing-attribute] - magnified = h > image.shape[0] or w > image.shape[1] # pyrefly: ignore[missing-attribute] + shape = image.shape[0], image.shape[1] + w, h = _get_width_height(width, height, shape) + magnified = h > image.shape[0] or w > image.shape[1] pixelated2 = pixelated if pixelated is not None else magnified html_strings.append( html_from_compressed_image( - png_data, w, h, title=title, border=border, pixelated=pixelated2 # pyrefly: ignore[bad-argument-type] + png_data, w, h, title=title, border=border, pixelated=pixelated2 ) ) # Create single-row tables each with no more than 'columns' elements. @@ -1340,10 +1337,7 @@ def _get_video_metadata(path: _Path) -> VideoMetadata: # will try to parse the framerate as x1000. if match := re.search(r', ([\d.]+)(k?) fps', line): number = float(match.group(1)) - if match.group(2) == 'k': - fps = number * 1000 - else: - fps = number + fps = number * 1000 if match.group(2) == 'k' else number elif str(path).endswith('.gif'): # Some GIF files lack a framerate attribute; use a reasonable default. fps = 10 @@ -1372,7 +1366,7 @@ class _VideoIO: def _get_pix_fmt(self, dtype: _DType, image_format: str) -> str: """Returns ffmpeg pix_fmt given data type and image format.""" native_endian_suffix = {'little': 'le', 'big': 'be'}[sys.byteorder] - return { + pix_fmt_map: dict[type[np.generic], dict[str, str]] = { np.uint8: { 'rgb': 'rgb24', 'yuv': 'yuv444p', @@ -1383,7 +1377,9 @@ def _get_pix_fmt(self, dtype: _DType, image_format: str) -> str: 'yuv': 'yuv444p16' + native_endian_suffix, 'gray': 'gray16' + native_endian_suffix, }, - }[dtype.type][image_format] # pyrefly: ignore[bad-index] + } + scalar_type = typing.cast(type[np.generic], dtype.type) + return pix_fmt_map[scalar_type][image_format] class VideoReader(_VideoIO): @@ -1459,7 +1455,7 @@ def __init__( self._popen: subprocess.Popen[bytes] | None = None self._proc: subprocess.Popen[bytes] | None = None - def __enter__(self) -> 'VideoReader': + def __enter__(self) -> VideoReader: try: self._read_via_local_file = _read_via_local_file(self.path_or_url) # pylint: disable-next=no-member @@ -1640,7 +1636,7 @@ def __init__( raise ValueError(f'Frame-per-second value {fps} is invalid.') if bps is None and metadata: bps = metadata.bps - bps = int(bps) if bps is not None else None + bps = bps if bps is not None else None if bps is not None and bps <= 0: raise ValueError(f'Bitrate value {bps} is invalid.') if qp is not None and (not isinstance(qp, int) or qp < 0): @@ -1706,7 +1702,7 @@ def __init__( self._popen: subprocess.Popen[bytes] | None = None self._proc: subprocess.Popen[bytes] | None = None - def __enter__(self) -> 'VideoWriter': + def __enter__(self) -> VideoWriter: input_pix_fmt = self._get_pix_fmt(self.dtype, self.input_format) try: self._write_via_local_file = _write_via_local_file(self.path) @@ -1823,16 +1819,16 @@ def close(self) -> None: self._write_via_local_file = None -class _VideoArray(np.ndarray): +class _VideoArray(npt.NDArray[Any]): """Wrapper to add a VideoMetadata `metadata` attribute to a numpy array.""" metadata: VideoMetadata | None def __new__( - cls: typing.Type['_VideoArray'], + cls: type[_VideoArray], input_array: _NDArray, metadata: VideoMetadata | None = None, - ) -> '_VideoArray': + ) -> _VideoArray: obj: _VideoArray = np.asarray(input_array).view(cls) obj.metadata = metadata return obj @@ -2056,13 +2052,14 @@ def show_videos( Returns: html string if `return_html` is `True`. """ + list_titles: list[str | None] if isinstance(videos, Mapping): if titles is not None: raise ValueError( 'Cannot have both a video dictionary and a titles parameter.' ) - list_titles = list(videos.keys()) list_videos = list(videos.values()) + list_titles = typing.cast(list[str | None], list(videos.keys())) else: list_videos = list(typing.cast('Iterable[_NDArray]', videos)) list_titles = [None] * len(list_videos) if titles is None else list(titles) @@ -2078,11 +2075,13 @@ def show_videos( for video, title in zip(list_videos, list_titles): metadata: VideoMetadata | None = getattr(video, 'metadata', None) first_image, video = _peek_first(video) - w, h = _get_width_height(width, height, first_image.shape[:2]) - if downsample and (w < first_image.shape[1] or h < first_image.shape[0]): + first_h, first_w = first_image.shape[:2] + w, h = _get_width_height(width, height, (first_h, first_w)) + if downsample and (w < first_w or h < first_h): # Not resize_video() because each image may have different depth and type. video = [resize_image(image, (h, w)) for image in video] first_image = video[0] + first_h, first_w = first_image.shape[:2] data = compress_video( video, metadata=metadata, fps=fps, bps=bps, qp=qp, codec=codec ) @@ -2092,13 +2091,13 @@ def show_videos( with _open(path, mode='wb') as f: f.write(data) if codec == 'gif': - pixelated = h > first_image.shape[0] or w > first_image.shape[1] + pixelated = h > first_h or w > first_w html_string = html_from_compressed_image( - data, w, h, title=title, fmt='gif', pixelated=pixelated, **kwargs # pyrefly: ignore[bad-argument-type] + data, w, h, title=title, fmt='gif', pixelated=pixelated, **kwargs ) else: html_string = html_from_compressed_video( - data, w, h, title=title, **kwargs # pyrefly: ignore[bad-argument-type] + data, w, h, title=title, **kwargs ) html_strings.append(html_string) diff --git a/pyproject.toml b/pyproject.toml index db1ea9a..21973e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,8 +99,28 @@ good-names-rgxs = "^[a-z][a-z0-9]?|[A-Z]([A-Z_]*[A-Z])?$" indent-string = " " expected-line-ending-format = "LF" +[tool.pyrefly] +min-severity = "warn" +project-excludes = ["*.ipynb"] + [tool.pytype] keep_going = true strict_none_binding = true use_enum_overlay = true use_fiddle_overlay = true + +[tool.ruff] +extend-exclude = ["*.ipynb"] + +[tool.ruff.lint] +select = [ + "E", "W", "F", "B", "C", "UP", "SIM", "RUF", "NPY", "PTH", "PERF", + "ISC", "ICN", "PIE", "RET", "TID", "TC", "I", +] +extend-ignore = [ + "E402", "E501", "E721", "E741", + "B010", "B028", "C408", "C901", + "TC003", "TC006", "SIM300", "ICN001", "RET504", "SIM905", "RUF005", + "I001", "SIM117", +] +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?)|unused_.*)$" From 33441ec3a10478c1d4c7520f88a959d89b17c947 Mon Sep 17 00:00:00 2001 From: Hugues Hoppe Date: Fri, 24 Jul 2026 19:29:52 -0700 Subject: [PATCH 05/14] Introduce _BaseArray to satisfy both mypy and Python 3.14+. --- mediapy/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mediapy/__init__.py b/mediapy/__init__.py index 13e995b..6f98bdb 100644 --- a/mediapy/__init__.py +++ b/mediapy/__init__.py @@ -179,12 +179,14 @@ _DTypeLike = npt.DTypeLike _NDArray = npt.NDArray[Any] _DType = np.dtype[Any] + _BaseArray = npt.NDArray[Any] # Satisfies mypy. else: # Create named types for use in the `pdoc` documentation. _ArrayLike = typing.TypeVar('_ArrayLike') _DTypeLike = typing.TypeVar('_DTypeLike') _NDArray = typing.TypeVar('_NDArray') _DType = typing.TypeVar('_DType') # pylint: disable=invalid-name + _BaseArray = np.ndarray # Avoids runtime errors in Python 3.14+. _IPYTHON_HTML_SIZE_LIMIT = 10**10 # Unlimited seems to be OK now. _T = typing.TypeVar('_T') @@ -1819,7 +1821,7 @@ def close(self) -> None: self._write_via_local_file = None -class _VideoArray(npt.NDArray[Any]): +class _VideoArray(_BaseArray): """Wrapper to add a VideoMetadata `metadata` attribute to a numpy array.""" metadata: VideoMetadata | None From a96e442cdcdc611999ca5ff39cae7bcc5bdf7467 Mon Sep 17 00:00:00 2001 From: Hugues Hoppe Date: Tue, 28 Jul 2026 10:50:13 -0700 Subject: [PATCH 06/14] Add pyrefly comment --- mediapy_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mediapy_test.py b/mediapy_test.py index 513d8a4..dabde49 100755 --- a/mediapy_test.py +++ b/mediapy_test.py @@ -423,6 +423,7 @@ def gray(x): if hasattr(matplotlib, 'colormaps'): cmap = matplotlib.colormaps['gray'] # Newer version. else: + # pyrefly: ignore[missing-attribute] cmap = matplotlib.pyplot.cm.get_cmap('gray') # pylint: disable=no-member return cmap(x)[..., :3] # pyrefly: ignore[bad-index] From 7e7894c7555e818e460565d00ee45bfff4f790c8 Mon Sep 17 00:00:00 2001 From: Hugues Hoppe Date: Wed, 29 Jul 2026 15:58:57 -0700 Subject: [PATCH 07/14] Modernize Python types, and adjust lint settings --- mediapy/__init__.py | 14 +++++++------- pyproject.toml | 4 +++- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/mediapy/__init__.py b/mediapy/__init__.py index 6f98bdb..ee55fa2 100644 --- a/mediapy/__init__.py +++ b/mediapy/__init__.py @@ -127,7 +127,7 @@ import urllib.request import warnings from collections.abc import Callable, Generator, Iterable, Iterator, Mapping, Sequence -from typing import Any +from typing import Any, TypeVar import IPython.display import matplotlib.pyplot @@ -182,15 +182,15 @@ _BaseArray = npt.NDArray[Any] # Satisfies mypy. else: # Create named types for use in the `pdoc` documentation. - _ArrayLike = typing.TypeVar('_ArrayLike') - _DTypeLike = typing.TypeVar('_DTypeLike') - _NDArray = typing.TypeVar('_NDArray') - _DType = typing.TypeVar('_DType') # pylint: disable=invalid-name + _ArrayLike = TypeVar('_ArrayLike') + _DTypeLike = TypeVar('_DTypeLike') + _NDArray = TypeVar('_NDArray') + _DType = TypeVar('_DType') # pylint: disable=invalid-name _BaseArray = np.ndarray # Avoids runtime errors in Python 3.14+. _IPYTHON_HTML_SIZE_LIMIT = 10**10 # Unlimited seems to be OK now. -_T = typing.TypeVar('_T') -_Path = typing.Union[str, 'os.PathLike[str]'] +_T = TypeVar('_T') +_Path = str | os.PathLike[str] _IMAGE_COMPARISON_HTML = """\