Context
The mypy adoption (PR #127) leaves the source tree green but with 187 # type: ignore comments in src/. This issue tracks reducing them to ~30. A detailed catalogue lives in TODO_type_cleanup.md at the root of the branch (untracked there). Summary below.
Four named structural problems
Problem 1 — Mutable default args + wrong Optional on InstrumentModelBase
src/instrumentserver/gui/base_instrument.py (~line 195). itemsStar, itemsTrash, itemsHide declared as Optional[List[str]] = []. Two bugs:
- Mutable default — all instances share the same list.
- Wrong
Optional — annotation says nullable but consumer requires List[str].
Fix: default None, branch on assignment, store as List[str] (non-Optional).
Problem 2 — parent overloaded as model OR item in addItem/insertItemTo
base_instrument.py (~line 298). insertItemTo declares parent: QStandardItem but is called with self (the model) at the root. Function body already handles both. Fix: make signature honest with Union[\"InstrumentModelBase\", QtGui.QStandardItem] and annotate the local parent in addItem / fillCollapsedDict likewise.
Problem 3 — Lock vs RLock clash in _createInstrument
src/instrumentserver/server/core.py:448-455. _get_lock_for_target returns Optional[RLock]; fallback uses _instrument_locks_lock which is Lock. Two ignores stack up. Fix: change _instrument_locks_lock to RLock (behavior-preserving — only acquired non-recursively at line 631) and simplify the call site.
Problem 4 — isinstance(obj, tuple(LIST_CONST)) defeats narrowing
server/core.py:518-529 and blueprints.py:340. Mypy can't inspect runtime-constructed tuples, so obj doesn't narrow. Fix: inline class tuples as literals at narrowing call sites (e.g. isinstance(obj, (Instrument, InstrumentChannel, InstrumentBase))). Module-level *_BASE_CLASSES lists can stay for the matching loops.
Catalog by root-cause bucket
| Bucket |
Pattern |
Approx count |
Fix sketch |
| A |
Qt event-handler [override] (PyQt5 declares QEvent | None) |
16 |
Change signature to Optional[QEvent], add early return |
| B |
PyQt5 "Optional return" widget/item stubs (addToolBar, header, parentWidget, itemFromIndex, verticalScrollBar, etc.) |
~89 |
Assign to local, assert is not None once, use locally |
| C |
Qt int-flag OR for non-setAlignment/findItems sites (DropAction, OpenModeFlag, etc.) |
~5 |
cast(\"QtCore.Qt.SomeFlags\", flag_a | flag_b) — see recipe note below |
| D |
pyqtBoundSignal.connect(Callable[..., bool]) arg-type |
~5 |
Wrap in discarding lambda |
| E |
ServerGui.stationServer/stationServerThread initialized to None |
17 |
Construct eagerly, or add _require_server() helper |
| F |
ProxyMixin.cli: Optional[Client] not narrowed in subclass |
~5 |
Redeclare cli: Client on ProxyInstrumentModule, or use property |
| G |
QStandardItem | None vs custom ItemBase subclass |
~19 |
typing.cast(ItemBase, item) at the boundary (one site already done in server/application.py:411-422 via cast(PossibleInstrumentDisplayItem, raw_item)) |
| H |
Cross-references to Problems 1–4 above |
— |
See above |
| I |
qcodes TSubmodule TypeVar bound rejects ProxyInstrumentModule/ParameterManager |
3 |
Won't go away — upstream qcodes constraint |
| J |
self.layout = QVBoxLayout(...) shadows QWidget.layout() method |
4 |
Rename self.layout → self._layout in ServerStatus |
| K |
PollingWorker.pollingRates: Optional[Dict] always-present-at-runtime |
5 |
Store as Dict[str, int] with {} default |
| L |
ClientStation.disconnect sets client = None over non-Optional declaration |
1 |
Declare as Optional[Client] and narrow at use sites |
| M |
BaseClient.socket: Optional[zmq.Socket] — narrowing not tracked |
2 |
Early assert self.socket is not None in ask() |
| N |
Misc small issues (method-assign, dict-lookup cast, Optional paths through paramsToFile, blueprints.py Optional dict iter, broadcast socket Optionals on the non-_broadcastParameterChange paths, etc.) |
~8 |
Case-by-case |
Recipe note for Bucket C
PyQt5 stubs do not have __or__ overloads on flag enums (AlignmentFlag, MatchFlag, DropAction, OpenModeFlag), so flag_a | flag_b evaluates to int. The Qt.Alignment(...) / Qt.MatchFlags(...) constructor only accepts Alignment | AlignmentFlag / MatchFlags | MatchFlag — not int — so wrapping with the constructor doesn't help. Use typing.cast instead:
from typing import cast
self.setAlignment(
cast(
\"QtCore.Qt.Alignment\",
QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter,
)
)
Same shape for findItems(..., cast(\"QtCore.Qt.MatchFlags\", flag_a | flag_b), ...).
Recommended priority
- High value, low risk (target: ~−40 ignores). Buckets J, M, the rest of G + Problems 1–4. Mostly mechanical, no wider refactor.
- Medium value, some refactor (target: ~−90 ignores). Buckets E (require-helper), F (subclass redeclare), B (narrow-once locals — bulk).
- Low value / project-style decisions. Buckets A, C (remaining sites), D — uniform but verbose changes; decide consistency rules first.
- Won't go away. Bucket I (qcodes upstream), the handful of intentional ignores in Bucket N (
method-assign for an intentional monkey-patch in client/proxy.py, import-not-found for optional influxdb_client, no-untyped-def on getWorkingDirectory per existing notes).
Target: 187 → ~30 ignores after (1) + (2) + Problems 1–4.
Test gap to close before refactoring InstrumentModelBase
Nothing in test/pytest/ directly exercises InstrumentModelBase.addItem / insertItemTo, the itemsHide/itemsStar/itemsTrash filter branches, or fillCollapsedDict/restoreCollapsedDict. Before doing Problems 1–2 it is worth adding:
InstrumentParameters constructed with parameters-hide=[...] — assert filter works.
- Two
ModelParameters instances with no itemsStar kwarg — assert m1.itemsStar is not m2.itemsStar (guards the mutable-default fix).
Suggested PR breakdown
To keep diffs reviewable, ship as small successive PRs:
- PR 1: Bucket J + M + rest of G (low risk, ~−25)
- PR 2: Problems 1–4 + new
InstrumentModelBase tests (~−15)
- PR 3: Bucket E (
stationServer require-helper, ~−17)
- PR 4: Bucket F (
ProxyInstrumentModule.cli redeclare, ~−5)
- PR 5: Bucket B (bulk widget-return narrowing, ~−70)
- PR 6: Decide and apply Buckets A/C/D project-wide
Related
Context
The
mypyadoption (PR #127) leaves the source tree green but with 187# type: ignorecomments insrc/. This issue tracks reducing them to ~30. A detailed catalogue lives inTODO_type_cleanup.mdat the root of the branch (untracked there). Summary below.Four named structural problems
Problem 1 — Mutable default args + wrong
OptionalonInstrumentModelBasesrc/instrumentserver/gui/base_instrument.py(~line 195).itemsStar,itemsTrash,itemsHidedeclared asOptional[List[str]] = []. Two bugs:Optional— annotation says nullable but consumer requiresList[str].Fix: default
None, branch on assignment, store asList[str](non-Optional).Problem 2 —
parentoverloaded as model OR item inaddItem/insertItemTobase_instrument.py(~line 298).insertItemTodeclaresparent: QStandardItembut is called withself(the model) at the root. Function body already handles both. Fix: make signature honest withUnion[\"InstrumentModelBase\", QtGui.QStandardItem]and annotate the localparentinaddItem/fillCollapsedDictlikewise.Problem 3 —
LockvsRLockclash in_createInstrumentsrc/instrumentserver/server/core.py:448-455._get_lock_for_targetreturnsOptional[RLock]; fallback uses_instrument_locks_lockwhich isLock. Two ignores stack up. Fix: change_instrument_locks_locktoRLock(behavior-preserving — only acquired non-recursively at line 631) and simplify the call site.Problem 4 —
isinstance(obj, tuple(LIST_CONST))defeats narrowingserver/core.py:518-529andblueprints.py:340. Mypy can't inspect runtime-constructed tuples, soobjdoesn't narrow. Fix: inline class tuples as literals at narrowing call sites (e.g.isinstance(obj, (Instrument, InstrumentChannel, InstrumentBase))). Module-level*_BASE_CLASSESlists can stay for the matching loops.Catalog by root-cause bucket
[override](PyQt5 declaresQEvent | None)Optional[QEvent], add early returnaddToolBar,header,parentWidget,itemFromIndex,verticalScrollBar, etc.)assert is not Noneonce, use locallysetAlignment/findItemssites (DropAction,OpenModeFlag, etc.)cast(\"QtCore.Qt.SomeFlags\", flag_a | flag_b)— see recipe note belowpyqtBoundSignal.connect(Callable[..., bool])arg-typeServerGui.stationServer/stationServerThreadinitialized toNone_require_server()helperProxyMixin.cli: Optional[Client]not narrowed in subclasscli: ClientonProxyInstrumentModule, or use propertyQStandardItem | Nonevs customItemBasesubclasstyping.cast(ItemBase, item)at the boundary (one site already done inserver/application.py:411-422viacast(PossibleInstrumentDisplayItem, raw_item))TSubmoduleTypeVar bound rejectsProxyInstrumentModule/ParameterManagerself.layout = QVBoxLayout(...)shadowsQWidget.layout()methodself.layout→self._layoutinServerStatusPollingWorker.pollingRates: Optional[Dict]always-present-at-runtimeDict[str, int]with{}defaultClientStation.disconnectsetsclient = Noneover non-Optional declarationOptional[Client]and narrow at use sitesBaseClient.socket: Optional[zmq.Socket]— narrowing not trackedassert self.socket is not Noneinask()method-assign, dict-lookupcast,Optionalpaths throughparamsToFile,blueprints.pyOptional dict iter, broadcast socket Optionals on the non-_broadcastParameterChangepaths, etc.)Recipe note for Bucket C
PyQt5 stubs do not have
__or__overloads on flag enums (AlignmentFlag,MatchFlag,DropAction,OpenModeFlag), soflag_a | flag_bevaluates toint. TheQt.Alignment(...)/Qt.MatchFlags(...)constructor only acceptsAlignment | AlignmentFlag/MatchFlags | MatchFlag— notint— so wrapping with the constructor doesn't help. Usetyping.castinstead:Same shape for
findItems(..., cast(\"QtCore.Qt.MatchFlags\", flag_a | flag_b), ...).Recommended priority
method-assignfor an intentional monkey-patch inclient/proxy.py,import-not-foundfor optionalinfluxdb_client,no-untyped-defongetWorkingDirectoryper existing notes).Target: 187 → ~30 ignores after (1) + (2) + Problems 1–4.
Test gap to close before refactoring
InstrumentModelBaseNothing in
test/pytest/directly exercisesInstrumentModelBase.addItem/insertItemTo, theitemsHide/itemsStar/itemsTrashfilter branches, orfillCollapsedDict/restoreCollapsedDict. Before doing Problems 1–2 it is worth adding:InstrumentParametersconstructed withparameters-hide=[...]— assert filter works.ModelParametersinstances with noitemsStarkwarg — assertm1.itemsStar is not m2.itemsStar(guards the mutable-default fix).Suggested PR breakdown
To keep diffs reviewable, ship as small successive PRs:
InstrumentModelBasetests (~−15)stationServerrequire-helper, ~−17)ProxyInstrumentModule.cliredeclare, ~−5)Related