Skip to content

Type-cleanup follow-up: drop 208 # type: ignore comments in src/ #128

@marcosfrenkel

Description

@marcosfrenkel

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:

  1. Mutable default — all instances share the same list.
  2. 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.layoutself._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 | MatchFlagnot 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

  1. High value, low risk (target: ~−40 ignores). Buckets J, M, the rest of G + Problems 1–4. Mostly mechanical, no wider refactor.
  2. Medium value, some refactor (target: ~−90 ignores). Buckets E (require-helper), F (subclass redeclare), B (narrow-once locals — bulk).
  3. Low value / project-style decisions. Buckets A, C (remaining sites), D — uniform but verbose changes; decide consistency rules first.
  4. 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:

  1. InstrumentParameters constructed with parameters-hide=[...] — assert filter works.
  2. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions