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: