Skip to content

Commit a226fc4

Browse files
authored
Merge pull request #29 from petercorke/fix/mixin-protocol-consistency
fix: bring 3 mixins in line with the TYPE_CHECKING _ImageBase pattern
2 parents 8c24de4 + 0d31fc5 commit a226fc4

4 files changed

Lines changed: 137 additions & 7 deletions

File tree

‎src/machinevisiontoolbox/ImageBlobs.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import webbrowser
1212
from collections import UserList, namedtuple
1313
from dataclasses import dataclass
14-
from typing import Any, cast
14+
from typing import TYPE_CHECKING, Any, cast
1515

1616
import cv2
1717
import matplotlib.pyplot as plt
@@ -24,6 +24,9 @@
2424
from machinevisiontoolbox.base import plot_labelbox, mpl_styling
2525
from machinevisiontoolbox.decorators import array_result, scalar_result
2626

27+
if TYPE_CHECKING:
28+
from machinevisiontoolbox._image_typing import _ImageBase
29+
2730
"""
2831
NOTES
2932
@@ -2623,7 +2626,7 @@ def dotfile(
26232626
dot_stream.close()
26242627

26252628

2626-
class ImageBlobsMixin:
2629+
class ImageBlobsMixin(_ImageBase if TYPE_CHECKING else object):
26272630
def blobs(self, **kwargs) -> Blobs:
26282631
"""
26292632
Find and describe blobs in image

‎src/machinevisiontoolbox/ImagePointFeatures.py‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"""
44

55
import math
6-
from typing import Any, Iterator
6+
from typing import TYPE_CHECKING, Any, Iterator
77

88
import cv2
99
import matplotlib.lines as mlines
@@ -21,13 +21,15 @@
2121
name2color,
2222
safe_plt_show,
2323
)
24-
from machinevisiontoolbox._image_typing import _ImageBase
2524
from machinevisiontoolbox.decorators import (
2625
array_result,
2726
array_result2,
2827
scalar_result,
2928
)
3029

30+
if TYPE_CHECKING:
31+
from machinevisiontoolbox._image_typing import _ImageBase
32+
3133
# TODO, either subclass SIFTFeature(BaseFeature2D) or just use BaseFeature2D
3234
# directly
3335

@@ -2012,7 +2014,7 @@ class LUCIDFeature(BaseFeature2D):
20122014
pass
20132015

20142016

2015-
class ImagePointFeaturesMixin(_ImageBase):
2017+
class ImagePointFeaturesMixin(_ImageBase if TYPE_CHECKING else object):
20162018
def _image2feature(
20172019
self,
20182020
cls,

‎src/machinevisiontoolbox/ImageRegionFeatures.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import numpy as np
1010
from ansitable import ANSITable, Column
1111
from spatialmath import SE3
12-
from typing import Any
12+
from typing import TYPE_CHECKING, Any
1313

1414
from machinevisiontoolbox.decorators import array_result
1515
from machinevisiontoolbox.base import plot_labelbox
@@ -22,8 +22,11 @@
2222
_pytesseract = None
2323
_pytesseract_available = False
2424

25+
if TYPE_CHECKING:
26+
from machinevisiontoolbox._image_typing import _ImageBase
2527

26-
class ImageRegionFeaturesMixin:
28+
29+
class ImageRegionFeaturesMixin(_ImageBase if TYPE_CHECKING else object):
2730
def MSER(self, **kwargs: Any) -> "MSERFeature":
2831
"""
2932
Find MSER features in image

‎tech-debt.md‎

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,100 @@
11
# Technical Debt
22

3+
## [HIGH PRIORITY] mypy is not run anywhere in CI or dev tooling
4+
5+
Found 2026-07-29 while fixing the `_ImageBase` Protocol gaps in
6+
`ImageBlobs.py`/`ImageRegionFeatures.py`/`ImagePointFeatures.py`
7+
(see git history — three mixins were missing the
8+
`class XMixin(_ImageBase if TYPE_CHECKING else object)` pattern every
9+
other mixin uses). That fix prompted the question: if a new method is
10+
added to a mixin and it's accessed via `self.` from a *different*
11+
mixin, and it's not yet declared in `_image_typing.py`'s `_ImageBase`
12+
Protocol, what actually catches that? Answer: **nothing, currently**.
13+
`mypy` is not in `pyproject.toml`'s `dev` extra and does not run in any
14+
`.github/workflows/*.yml` — confirmed by grep, zero hits. So a Protocol
15+
gap like this doesn't fail a build or even show a warning; it just
16+
silently produces incomplete/wrong type information for anyone using an
17+
editor with type-checking (Pylance, mypy in an IDE), with no automated
18+
signal anywhere. This is the quietest version of a pattern that's
19+
already bitten this project twice today in more visible forms (the
20+
`image_class.rst` autosummary list silently going stale for the whole
21+
`Image` sidebar, and `ci.yml`'s conda `create-args` list silently
22+
missing `pgraph-python`/drifting on `opencv`) — a hand-maintained
23+
shadow list with no automated check that it stays in sync with reality.
24+
25+
**Ran `mypy src/machinevisiontoolbox --ignore-missing-imports` fresh,
26+
2026-07-29** (superseding the stale, less-categorized April audit in
27+
`NOTES`): **524 errors in 31 files** (checked 49 source files). By
28+
category:
29+
30+
| Code | Count |
31+
|---|---|
32+
| `attr-defined` | 84 |
33+
| `assignment` | 82 |
34+
| `union-attr` | 76 |
35+
| `index` | 75 |
36+
| `arg-type` | 53 |
37+
| `misc` | 26 |
38+
| `name-defined` | 24 |
39+
| `var-annotated` | 18 |
40+
| `valid-type` | 15 |
41+
| `operator` | 15 |
42+
| `call-overload` | 14 |
43+
| `return-value` | 13 |
44+
| `has-type` | 11 |
45+
| `return` | 6 |
46+
| `no-redef` | 5 |
47+
| `override` | 4 |
48+
| `method-assign` | 2 |
49+
| `call-arg` | 1 |
50+
51+
**Correcting an initial hypothesis**: expected most `attr-defined`
52+
errors to trace to incomplete `_ImageBase` coverage (only ~96 of
53+
`Image`'s ~290 public members are declared — by design, since the
54+
Protocol only needs to cover attributes actually cross-referenced
55+
between mixins, not the full public API). Checked the real breakdown
56+
instead of assuming: **none** of the current `attr-defined` errors are
57+
actually `_ImageBase` gaps. The two real dominant causes are unrelated:
58+
- 23 of 84: `machinevisiontoolbox/base/__init__.py` re-exports every
59+
submodule via wildcard `from X import *` (9 submodules) with no
60+
explicit `__all__`/direct re-export list; mypy can't reliably resolve
61+
names through that chain, so every file that does
62+
`from machinevisiontoolbox.base import (draw_circle, plot_labelbox,
63+
findpeaks2d, ...)` gets a false "module has no attribute" even though
64+
these work fine at runtime.
65+
- 8 of 84: `cv2.<X>_create` dynamic dispatch (`getattr(cv2, ...)` /
66+
`getattr(cv2.xfeatures2d, ...)` patterns in `ImagePointFeatures.py`'s
67+
feature-detector dict) — mypy can't type-check dynamic attribute
68+
access, expected and low-value to fix.
69+
- The remaining ~53 are scattered; `VisualServo.py` alone accounts for
70+
44 of the 84 `attr-defined` errors (a mix of real typos like
71+
`"plotpose"; maybe "plot_pose"?` and missing `machinevisiontoolbox.base`
72+
attributes via the same wildcard-import issue) and is worth its own
73+
look independent of the mixin-Protocol question that prompted this
74+
audit.
75+
76+
The `_ImageBase`-completeness risk described above is still real, just
77+
currently *latent* rather than demonstrated by a live error — worth
78+
re-running this same `mypy` audit after any future mixin refactor to
79+
catch it if it does start manifesting.
80+
81+
### Fix
82+
83+
Two independent pieces, roughly in priority order:
84+
1. Fix `machinevisiontoolbox/base/__init__.py`'s wildcard re-exports
85+
(add explicit `__all__` composed from each submodule's own `__all__`,
86+
or switch to explicit `from X import (name1, name2, ...)` — either
87+
should immediately clear ~23+ of the `attr-defined` count and is a
88+
mechanical, low-risk change).
89+
2. Wire `mypy` into CI (even just as a non-blocking/advisory job at
90+
first, given 524 existing errors) so future drift is visible instead
91+
of silent. Add `mypy` to `pyproject.toml`'s `dev` extra either way.
92+
Do not attempt to fix all 524 errors in one pass — triage by category
93+
(the `[call-arg]`/`[override]`/`[return]` categories are more likely to
94+
be real bugs; `assignment`/`arg-type`/`index` are more likely the
95+
`ArrayLike`-union-too-broad pattern the April `NOTES` audit already
96+
identified).
97+
398
## `Image.ncdf` is documented as deprecated but never warns
499

5100
`ncdf` (`src/machinevisiontoolbox/ImageWholeFeatures.py:511-523`) has a
@@ -147,6 +242,33 @@ new extra or dependency. Patched directly (added `ipython`/`pygments` to
147242
`create-args`) and merged 2026-07-29 — the underlying architectural gap
148243
below is still open.
149244

245+
**Third example, found 2026-07-29 (pre-existing, not a new incident)**:
246+
`pgraph-python` is a real, declared dependency in `pyproject.toml`'s
247+
`dependencies` list, but is completely absent from `ci.yml`'s
248+
`create-args` — not merely unpinned, never installed at all. The only
249+
file that imports it, `BundleAdjust.py`, does so inside a bare
250+
`except:` (see the bare-except finding elsewhere in this file) that
251+
silently falls back when the import fails. So every CI run has been
252+
exercising `BundleAdjust`'s pgraph-dependent code in "not installed"
253+
fallback mode this whole time, with nothing surfacing it. Not fixed
254+
here — decided (2026-07-29) to leave the conda→pip migration itself as
255+
deferred tech debt rather than keep patching individual missing
256+
packages one at a time; noting this one so it's not lost.
257+
258+
**Decision, 2026-07-29**: known who introduced the conda/micromamba
259+
setup and why (a well-intentioned contributor's preference for conda,
260+
not a technical requirement) — confirmed not urgent enough to fix now.
261+
Branch protection on `main` (`All tests passed` + `build` required,
262+
strict/up-to-date-with-base required) now contains the *blast radius*
263+
of a future incident like this — it'll block just the one PR that hits
264+
it, rather than silently landing on `main` and confusing every other
265+
open PR at once, which was the actually painful part of the opencv5
266+
incident. The root architectural gap remains real and will very likely
267+
surface again the same way (a fourth missing/drifted package), but
268+
that's an acceptable trade for now given the fix requires careful
269+
cross-OS testing (libegl/Xvfb handling, `matplotlib-base` vs
270+
`matplotlib` naming) rather than a quick patch.
271+
150272
### Fix
151273

152274
Normalize to the same `actions/setup-python` + `pip install .[dev]`

0 commit comments

Comments
 (0)