From 2bba412d29ce46e504bb25e1c41e0829733d3a96 Mon Sep 17 00:00:00 2001 From: Alexander Refsum Jensenius Date: Sun, 23 Aug 2026 19:40:21 +0200 Subject: [PATCH] mypy reports nothing, so the CI step blocks merges now Closes #350. `mypy musicalgestures/` is clean across all 69 source files, from 248 when that issue was opened, and the `|| true` that made the step advisory is gone. No allowlist was needed in the end. The last eight were of a piece with the rest. `get_acceleration` declared `fps: int`, the same mistake `frame2ms` carried --- a frame rate is not an integer. Two GPU objects are created under a flag and used under the same flag, which mypy will not correlate. Three output names are resolved in one branch and read in another. And `_pose_mediapipe` and `_rerender_pose_from_cache` had no return annotation at all, so everything that returned their result returned Any. The version pin added in 3504a15 is what makes this safe to gate on. The count mypy reports moves with the checker as well as with the code: on identical source, 1.20.1 reports 169 errors where 2.3.1 reports 90. Blocking on an unpinned checker would let a release turn CI red with no change to the code. 682 tests pass, 4 skipped, on the same tree as the clean type check --- rerun after the motion_mp merge rather than carried over from before it. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 8 +++++--- CHANGELOG.md | 12 +++++++++--- musicalgestures/_enums.py | 2 +- musicalgestures/_flow.py | 7 ++++++- musicalgestures/_history.py | 2 +- musicalgestures/_motionvideo.py | 1 + musicalgestures/_pose.py | 5 +++-- musicalgestures/_pose_estimator.py | 1 + musicalgestures/_posetools.py | 1 + musicalgestures/_show.py | 1 + musicalgestures/_stream.py | 5 ++++- 11 files changed, 33 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 24b326fd..2b80619d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,6 +86,8 @@ jobs: run: ruff format --check musicalgestures/ || true - name: Mypy type check - # Non-blocking: the public API is typed (params + returns + py.typed), but the internals - # still have a backlog of errors. Settings (follow_imports/exclude) come from pyproject.toml. - run: mypy musicalgestures/ || true \ No newline at end of file + # Blocking since 2026-08-23. The internal backlog that kept this advisory is gone: it ran + # from 248 errors to zero over #350. Settings (follow_imports/exclude) come from + # pyproject.toml, and the version is pinned above, because the count this reports moves + # with the checker as well as with the code. + run: mypy musicalgestures/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b063977..8ff27cf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,9 +17,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 behind it are gone. Closes #370. ### Changed -- Internal typing, issue #350: `mypy musicalgestures/` is at 17 errors in 9 files, from 90 when - this work started. Retiring `motion_mp()` accounts for nine of the reduction, since typing code - that cannot run would have been effort spent hiding a breakage. +- **The mypy step in CI blocks merges now.** `mypy musicalgestures/` reports no issues across all + 69 source files, from 248 when issue #350 was opened, so the `|| true` that made it advisory is + gone. The checker version is pinned alongside it, because the count this reports moves with the + checker as well as with the code: on identical source, mypy 1.20.1 reported 169 errors where + 2.3.1 reported 90. Closes #350. +- The last of that backlog: two frame rates typed as integers, which they are not; two GPU objects + created under a flag and used under the same flag, which a checker will not correlate; three + output names resolved in one branch and used in another; and two functions carrying no return + annotation at all. ## [1.13.0] — 2026-08-23 diff --git a/musicalgestures/_enums.py b/musicalgestures/_enums.py index 1b6f7c3b..a378552b 100644 --- a/musicalgestures/_enums.py +++ b/musicalgestures/_enums.py @@ -21,7 +21,7 @@ class _StrEnumBase(str, Enum): # type: ignore[no-redef] """Backward-compatible StrEnum for Python 3.10.""" def __str__(self) -> str: - return self.value + return str(self.value) class _MgEnum(_StrEnumBase): diff --git a/musicalgestures/_flow.py b/musicalgestures/_flow.py index 92656c1e..8ac14b74 100644 --- a/musicalgestures/_flow.py +++ b/musicalgestures/_flow.py @@ -218,6 +218,9 @@ def dense( if ii == 0: out.stdin.write(rgb.astype(np.uint8)) else: + # frame 0 always sets prev_rgb below, and this arm only + # runs from frame 1 on, so it is set by the time we read it + assert prev_rgb is not None out.stdin.write(prev_rgb.astype(np.uint8)) else: out.stdin.write(rgb.astype(np.uint8)) @@ -287,6 +290,7 @@ def dense( else: out.stdin.close() out.wait() + assert target_name is not None # resolve_filename ran earlier in this branch destination_video = target_name if self.has_audio: @@ -300,7 +304,7 @@ def dense( return self._parent().flow_dense_video - def get_acceleration(self, velocity: list, fps: int): + def get_acceleration(self, velocity: list, fps: float): acceleration = np.zeros(len(velocity)) velocity = np.abs(velocity) @@ -450,6 +454,7 @@ def sparse( # calculate optical flow if _use_gpu: + assert lk_gpu is not None # created under the same flag above gpu_frame_gray.upload(frame_gray) gpu_p1, gpu_st, _gpu_err = lk_gpu.calc(gpu_old_gray, gpu_frame_gray, gpu_p0, None) # GPU returns 1xN; flatten to (N,2)/(N,) for uniform selection diff --git a/musicalgestures/_history.py b/musicalgestures/_history.py index 004a68f3..44de377d 100644 --- a/musicalgestures/_history.py +++ b/musicalgestures/_history.py @@ -151,7 +151,7 @@ def history_cv2(self, filename: str | None = None, history_length: int = 10, wei ii = 0 history: list = [] - weights_map = [1 for weight in range(history_length+1)] + weights_map: list[float] = [1 for weight in range(history_length+1)] if type(weights) in [int, float]: assert isinstance(weights, (int, float)) diff --git a/musicalgestures/_motionvideo.py b/musicalgestures/_motionvideo.py index 9efb2ebd..73d50fe8 100644 --- a/musicalgestures/_motionvideo.py +++ b/musicalgestures/_motionvideo.py @@ -344,6 +344,7 @@ def mg_motion( os.remove(source_audio) # Save generated musicalgestures video as the video of the parent MgVideo + assert target_name_video is not None # resolved under `if save_video` above self.motion_video = musicalgestures.MgVideo(filename=target_name_video, returned_by_process=True) return self.motion_video diff --git a/musicalgestures/_pose.py b/musicalgestures/_pose.py index 2e347dd6..c0f8560f 100644 --- a/musicalgestures/_pose.py +++ b/musicalgestures/_pose.py @@ -734,6 +734,7 @@ def save_single_file(of, width, height, model, data, data_format, target_name_da if save_video: # save result as pose_video for parent MgVideo + assert target_name_video is not None # resolved under `if save_video` above self.pose_video = musicalgestures.MgVideo(target_name_video, color=self.color, returned_by_process=True) self.pose_video.average_pose = average_image self.pose_video.trajectories = trajectories_image @@ -788,7 +789,7 @@ def _rerender_pose_from_cache(self: "musicalgestures.MgVideo", style='both', ove trajectory_labels=False, marker_history=0, target_name_video=None, target_name_data=None, target_name_average=None, - target_name_trajectories=None, overwrite=True): + target_name_trajectories=None, overwrite=True) -> "musicalgestures.MgVideo": """Re-render the pose outputs from cached keypoints (no network inference).""" c = self._pose_keypoints data, names, connections = c['data'], c['names'], c['connections'] @@ -1147,7 +1148,7 @@ def _pose_mediapipe( target_name_data=None, target_name_average=None, target_name_trajectories=None, - overwrite=True): + overwrite=True) -> "musicalgestures.MgVideo": """ Internal helper: run MediaPipe Pose on a video and render/save the output. Called by :func:`pose` when ``model='mediapipe'`` (or when GPU is requested and the diff --git a/musicalgestures/_pose_estimator.py b/musicalgestures/_pose_estimator.py index cd14762f..9182f660 100644 --- a/musicalgestures/_pose_estimator.py +++ b/musicalgestures/_pose_estimator.py @@ -407,6 +407,7 @@ def predict_frame(self, frame: np.ndarray) -> PoseEstimatorResult: rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=rgb) + assert self._landmarker is not None, "the landmarker was not initialised" detection_result = self._landmarker.detect(mp_image) n = len(MEDIAPIPE_LANDMARK_NAMES) diff --git a/musicalgestures/_posetools.py b/musicalgestures/_posetools.py index b1a9111e..67a2bec0 100644 --- a/musicalgestures/_posetools.py +++ b/musicalgestures/_posetools.py @@ -227,6 +227,7 @@ def _read_result(lms, wlms): proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) # stdout=PIPE guarantees a stream; the annotation is Optional for the general case assert proc.stdout is not None, "ffmpeg was started without a readable output stream" + assert proc.stderr is not None, "ffmpeg was started without a readable error stream" # Drain FFmpeg's stderr on a background thread as it is produced, rather # than only reading it in the `finally` block below: the stdout-reading # loop can run far longer than the OS pipe buffer takes to fill (a few diff --git a/musicalgestures/_show.py b/musicalgestures/_show.py index 48c881dd..c7519c07 100644 --- a/musicalgestures/_show.py +++ b/musicalgestures/_show.py @@ -143,6 +143,7 @@ def colab_display(video_to_display, video_width, video_height): horizontal_keys = ('horizontal', 'mgh', 'vgh', 'mgy', 'vgy') orientation = 'horizontal' if k in horizontal_keys else 'vertical' label = orientation.capitalize() + kinds: tuple[str, ...] if k in ('mgh', 'mgv', 'mgx', 'mgy'): kinds = ('motiongram',) elif k in ('vgh', 'vgv', 'vgx', 'vgy'): diff --git a/musicalgestures/_stream.py b/musicalgestures/_stream.py index 7dbac751..09a191f2 100644 --- a/musicalgestures/_stream.py +++ b/musicalgestures/_stream.py @@ -156,7 +156,8 @@ def __enter__(self) -> "MgVideoReader": def __exit__(self, *_) -> None: if self._process is not None: try: - self._process.stdout.close() + if self._process.stdout is not None: + self._process.stdout.close() self._process.wait(timeout=5) except Exception: self._process.kill() @@ -177,6 +178,8 @@ def __iter__(self) -> Generator[tuple[np.ndarray, float], None, None]: frame_bytes = self._height * self._width * channels fps = self._fps + # opened with stdout=PIPE in __enter__, which the guard above confirms ran + assert self._process.stdout is not None while True: raw = self._process.stdout.read(frame_bytes) if len(raw) < frame_bytes: