Skip to content

Commit faef80e

Browse files
authored
Enable mypy strict-optional and fix nullability gaps (closes #69) (#79)
* initial implementation of issue#69 * fix: guard or narrow row before indexing it.
1 parent 014b4e1 commit faef80e

6 files changed

Lines changed: 18 additions & 16 deletions

File tree

.github/workflows/tests.yml

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,10 @@ jobs:
135135

136136
# ── Typecheck: mypy ───────────────────────────────────────────────────────
137137
# Codebase already has type hints across most of the surface (~70+ typed
138-
# functions). Mypy runs in lenient mode (--ignore-missing-imports for
139-
# untyped third-party deps; no strict-optional) so the gate isn't a wall
140-
# of false positives. The transitional `continue-on-error: true` was
141-
# removed in #29 once mypy reached zero errors on this repo — type
142-
# failures now block merges.
138+
# functions). Mypy runs with --ignore-missing-imports for untyped
139+
# third-party deps; strict-optional is enabled (mypy default). The
140+
# transitional `continue-on-error: true` was removed in #29 once mypy
141+
# reached zero errors on this repo — type failures now block merges.
143142
typecheck:
144143
name: Typecheck (mypy)
145144
runs-on: ubuntu-latest
@@ -162,7 +161,7 @@ jobs:
162161
- name: Run mypy
163162
# No `continue-on-error` — mypy now exits zero on this repo (closes #29),
164163
# so type errors must fail the job from here on.
165-
run: mypy --ignore-missing-imports --no-strict-optional --pretty .
164+
run: mypy --ignore-missing-imports --pretty .
166165

167166
# ── Secret scan: gitleaks ─────────────────────────────────────────────────
168167
# Catches accidentally committed credentials. Runs over full git history

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ include = [
8888
# and CI produce identical results.
8989
[tool.mypy]
9090
ignore_missing_imports = true
91-
no_strict_optional = true
9291
pretty = true
9392
# Exclude virtual-env and build artefact directories so that `mypy .` from the
9493
# repo root matches CI behaviour (CI runs in a clean runner without a local venv).

services/workspace_listing.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,11 @@ def _safe_fetchall(query: str, params: tuple = ()) -> list:
119119

120120
headers = cd.get("fullConversationHeadersOnly") or []
121121
has_bubbles = any(
122-
bubble_map.get(h.get("bubbleId"))
122+
bubble_map.get(bubble_id)
123123
for h in headers
124124
if isinstance(h, dict)
125+
for bubble_id in [h.get("bubbleId")]
126+
if isinstance(bubble_id, str)
125127
)
126128
if not has_bubbles:
127129
continue

services/workspace_resolver.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,10 @@ def _infer_workspace_name_from_context(workspace_path: str, workspace_id: str) -
8989
except sqlite3.Error:
9090
continue
9191
for row in rows:
92+
if not row or row[0] is None:
93+
continue
9294
try:
93-
# sqlite3.Row supports string-key access at runtime when
94-
# row_factory = sqlite3.Row is set on the connection (see
95-
# _open_global_db); mypy's stub types Row as tuple[Any, ...]
96-
# which only accepts SupportsIndex, hence the ignore.
97-
ctx = json.loads(row["value"]) # type: ignore[call-overload]
95+
ctx = json.loads(row[0])
9896
except Exception:
9997
continue
10098
layouts = ctx.get("projectLayouts")

services/workspace_tabs.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,8 @@ def _safe_fetchall(query: str, params: tuple = ()) -> list:
282282
if not isinstance(header, dict):
283283
continue
284284
bubble_id = header.get("bubbleId")
285+
if not isinstance(bubble_id, str):
286+
continue
285287
bubble = bubble_map.get(bubble_id)
286288
if not bubble:
287289
continue
@@ -548,10 +550,11 @@ def _safe_fetchall(query: str, params: tuple = ()) -> list:
548550
if model_name_from_config and model_name_from_config != "default":
549551
if not tab_meta:
550552
tab_meta = {}
551-
if not tab_meta.get("modelsUsed"):
553+
models_used = tab_meta.get("modelsUsed")
554+
if not isinstance(models_used, list):
552555
tab_meta["modelsUsed"] = [model_name_from_config]
553-
elif model_name_from_config not in tab_meta["modelsUsed"]:
554-
tab_meta["modelsUsed"].insert(0, model_name_from_config)
556+
elif model_name_from_config not in models_used:
557+
models_used.insert(0, model_name_from_config)
555558

556559
tab = {
557560
"id": composer_id,

tests/test_workspace_tabs_malformed_nested.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ def test_diffs_appear_in_code_block_diffs_field(self) -> None:
131131

132132
tab = next((t for t in payload["tabs"] if t["id"] == "cmp-d"), None)
133133
self.assertIsNotNone(tab)
134+
assert tab is not None
134135
self.assertTrue(tab["codeBlockDiffs"], "expected diffs on tab.codeBlockDiffs")
135136

136137
def test_diffs_do_not_appear_as_synthetic_bubbles(self) -> None:

0 commit comments

Comments
 (0)