From 6e4645d09f3adf30b53495c00203eaed7fc3ed15 Mon Sep 17 00:00:00 2001 From: Alexander Refsum Jensenius Date: Sun, 23 Aug 2026 18:04:42 +0200 Subject: [PATCH] Stop storing a figure that was never built, and reach the floor #370 sets #350's tail: mypy 34 to 26. This is where the remaining count stops being a matter of effort. *Four pose methods stored None as though it were a figure.* Since the render helpers gained honest return types, `self.pose_waterfall_figure = mgf` could write None into an attribute declared to hold an MgFigure. The assignment is now conditional: a figure that could not be built leaves the attribute absent, which is what the whole declared-attribute model means and what keeps `hasattr` truthful. Storing None would have been found by `show()` and then failed on it. *Three lazily built things, guarded by a flag mypy will not correlate.* The GPU Farneback object is created under `_use_gpu` and used under `_use_gpu`; the two Eulerian lowpass pyramids are built together on the first frame and used together afterwards. Asserted where they are used, and annotated where they are declared. THE FLOOR. Nine of the twenty-six that remain are inside `motion_mp()`, which raises on its first call --- issue #370. Typing code that cannot run would be work spent to hide a breakage, so those are left exactly as they are. Seventeen are addressable without touching it, which means zero is not reachable while #370 stands. Whoever takes the `|| true` off the CI step is choosing between repairing that function, retiring it, and allowlisting the one file. 682 tests pass, 4 skipped. Checked by hand that both Eulerian modes still run, including the motion mode that uses the pyramids above. Co-Authored-By: Claude Opus 5 --- musicalgestures/_eulerian.py | 6 ++++-- musicalgestures/_flow.py | 1 + musicalgestures/_pose.py | 20 ++++++++++++++++---- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/musicalgestures/_eulerian.py b/musicalgestures/_eulerian.py index a6e6ef44..710355f5 100644 --- a/musicalgestures/_eulerian.py +++ b/musicalgestures/_eulerian.py @@ -169,8 +169,8 @@ def _open_writer(): process = _open_reader() writer = _open_writer() - lowpass1 = None - lowpass2 = None + lowpass1: "list | None" = None + lowpass2: "list | None" = None # IIR cutoffs derived from the requested band r1 = freq_high * 2.0 / fps r2 = freq_low * 2.0 / fps @@ -188,6 +188,8 @@ def _open_writer(): lowpass2 = [lvl.copy() for lvl in pyr] filtered = [np.zeros_like(lvl) for lvl in pyr] else: + # built together on the first frame, above, and used together here + assert lowpass1 is not None and lowpass2 is not None for k in range(len(pyr)): lowpass1[k] = (1 - r1) * lowpass1[k] + r1 * pyr[k] lowpass2[k] = (1 - r2) * lowpass2[k] + r2 * pyr[k] diff --git a/musicalgestures/_flow.py b/musicalgestures/_flow.py index 6bf02adc..92656c1e 100644 --- a/musicalgestures/_flow.py +++ b/musicalgestures/_flow.py @@ -183,6 +183,7 @@ def dense( next_frame = cv2.cvtColor(cv2.resize(frame2, size), cv2.COLOR_BGR2GRAY) if _use_gpu: + assert farneback_gpu is not None # created under the same flag above gpu_next_frame.upload(next_frame) gpu_flow_result = farneback_gpu.calc(gpu_prev_frame, gpu_next_frame, None) flow = gpu_flow_result.download() diff --git a/musicalgestures/_pose.py b/musicalgestures/_pose.py index d5c0bd4e..2e347dd6 100644 --- a/musicalgestures/_pose.py +++ b/musicalgestures/_pose.py @@ -969,7 +969,10 @@ def mg_pose_waterfall(self: "musicalgestures.MgVideo", style: str = 'trajectorie overwrite=overwrite, style=style, connections=c.get('connections'), n_samples=n_samples, markers=markers, color_by=color_by, cmap=cmap, dpi=dpi, elev=elev, azim=azim, lw=lw, axes=axes, crop=crop) - self.pose_waterfall_figure = mgf + # a figure that could not be built is not a result: leaving the attribute + # absent keeps `hasattr` honest, where storing None would not + if mgf is not None: + self.pose_waterfall_figure = mgf return mgf @@ -1014,7 +1017,10 @@ def mg_pose_segments(self: "musicalgestures.MgVideo", segments: list | None = No c['data'], c['names'], c.get('connections'), c['width'], c['height'], c['fps'], target_name, overwrite=overwrite, segments=segments, n_bins=n_bins, cmap=cmap, dpi=dpi, ncols=ncols) - self.pose_segments_figure = mgf + # a figure that could not be built is not a result: leaving the attribute + # absent keeps `hasattr` honest, where storing None would not + if mgf is not None: + self.pose_segments_figure = mgf return mgf @@ -1065,7 +1071,10 @@ def mg_pose_center(self: "musicalgestures.MgVideo", save_data: bool = True, dpi: pd.DataFrame(cols).to_csv(os.path.splitext(target_name)[0] + '.csv', index=False) except Exception as e: print(f'Warning: could not save CSV: {e}') - self.pose_centered_figure = mgf + # a figure that could not be built is not a result: leaving the attribute + # absent keeps `hasattr` honest, where storing None would not + if mgf is not None: + self.pose_centered_figure = mgf return mgf @@ -1103,7 +1112,10 @@ def mg_pose_distance(self: "musicalgestures.MgVideo", dpi: int = 200, target_nam mgf = render_pose_distance(c['data'], c['names'], c['width'], c['height'], c['fps'], target_name, overwrite=overwrite, dpi=dpi) - self.pose_distance_figure = mgf + # a figure that could not be built is not a result: leaving the attribute + # absent keeps `hasattr` honest, where storing None would not + if mgf is not None: + self.pose_distance_figure = mgf return mgf