Skip to content

Commit 8c4c4ae

Browse files
committed
Fix blanket file-wide type ignores
1 parent 9a1ca70 commit 8c4c4ae

6 files changed

Lines changed: 158 additions & 95 deletions

File tree

CLAUDE.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,10 +151,14 @@ that is a code change in the read routes, not a test change.
151151
SQLModel declares columns as plain annotations (e.g. `id: int | None`) rather
152152
than `Mapped[int]`, so Pyright reads `Column == value` as `bool` and flags
153153
`where()`/`exec()`/`select()`/`selectinload` calls and `result.rowcount`. These
154-
are framework false positives. The three repository modules carry a documented
155-
file-level `# pyright: reportArgumentType=false, reportCallIssue=false,
156-
reportAttributeAccessIssue=false` directive; other rules stay enabled so real
157-
bugs still surface. Keep `api/` and `tests/` at zero Pyright errors.
154+
are framework false positives. Suppress them with **targeted, inline**
155+
`# pyright: ignore[<rule>]` comments at the specific offending call sites (e.g.
156+
`# pyright: ignore[reportArgumentType]` on a `.where(Column == value)` line) —
157+
not blanket file-level `# pyright:` directives, which would hide genuine errors
158+
of those rules elsewhere in the file. Note Black may wrap a long query line and
159+
move a trailing comment off the flagged line; place the ignore on the line
160+
Pyright actually reports (often the inner `== value` line) so it survives
161+
formatting. Keep `api/` and `tests/` at zero Pyright errors.
158162

159163
### Alembic enum migrations
160164

api/src/tasking/projects/repository.py

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,3 @@
1-
# SQLModel declares columns as plain annotations (e.g. ``id: int | None``) rather
2-
# than ``Mapped[int]``, so Pyright reads ``Column == value`` as ``bool`` and
3-
# misjudges ``where()``/``select()``/``selectinload`` calls; the nullable
4-
# ``deleted_at`` column additionally trips ``reportOptionalMemberAccess`` on
5-
# ``.is_(None)``. These are framework false positives; the queries are valid at
6-
# runtime. Other rules stay enabled so real bugs still surface.
7-
# pyright: reportArgumentType=false, reportCallIssue=false, reportAttributeAccessIssue=false, reportOptionalMemberAccess=false
8-
91
from __future__ import annotations
102

113
import json
@@ -96,7 +88,7 @@ def _aoi_to_shapely(aoi: AoiInput) -> ShapelyMultiPolygon:
9688
if not geom.is_valid:
9789
raise HTTPException(
9890
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
99-
detail=f"AOI is not a valid polygon: {geom.is_valid_reason if hasattr(geom, 'is_valid_reason') else 'self-intersection or invalid ring'}",
91+
detail=f"AOI is not a valid polygon: {geom.is_valid_reason if hasattr(geom, 'is_valid_reason') else 'self-intersection or invalid ring'}", # pyright: ignore[reportAttributeAccessIssue]
10092
)
10193

10294
return geom
@@ -221,7 +213,11 @@ async def _get_active(self, workspace_id: int, project_id: int) -> TaskingProjec
221213
select(TaskingProject).where(
222214
(TaskingProject.id == project_id)
223215
& (TaskingProject.workspace_id == workspace_id)
224-
& (TaskingProject.deleted_at.is_(None))
216+
& (
217+
TaskingProject.deleted_at.is_( # pyright: ignore[reportAttributeAccessIssue, reportOptionalMemberAccess]
218+
None
219+
)
220+
)
225221
)
226222
)
227223
project = result.scalar_one_or_none()
@@ -387,7 +383,9 @@ async def list_projects(
387383
col = col.desc() if order_dir.upper() == "DESC" else col.asc()
388384

389385
where = (TaskingProject.workspace_id == workspace_id) & (
390-
TaskingProject.deleted_at.is_(None)
386+
TaskingProject.deleted_at.is_( # pyright: ignore[reportAttributeAccessIssue, reportOptionalMemberAccess]
387+
None
388+
)
391389
)
392390
if status_filter is not None:
393391
where = where & (TaskingProject.status == status_filter)
@@ -634,7 +632,10 @@ async def patch(
634632
try:
635633
await self.session.execute(
636634
update(TaskingProject)
637-
.where(TaskingProject.id == project.id)
635+
.where(
636+
TaskingProject.id
637+
== project.id # pyright: ignore[reportArgumentType]
638+
)
638639
.values(**updates)
639640
)
640641
await self._audit(
@@ -678,7 +679,9 @@ async def soft_delete(
678679
# Soft-delete the project, hard-delete its tasks, flag audit rows.
679680
await self.session.execute(
680681
update(TaskingProject)
681-
.where(TaskingProject.id == project.id)
682+
.where(
683+
TaskingProject.id == project.id # pyright: ignore[reportArgumentType]
684+
)
682685
.values(deleted_at=datetime.now())
683686
)
684687
await self.session.execute(
@@ -748,7 +751,9 @@ async def activate(
748751

749752
await self.session.execute(
750753
update(TaskingProject)
751-
.where(TaskingProject.id == project.id)
754+
.where(
755+
TaskingProject.id == project.id # pyright: ignore[reportArgumentType]
756+
)
752757
.values(status=ProjectStatus.OPEN, updated_at=datetime.now())
753758
)
754759
await self._audit(
@@ -799,7 +804,9 @@ async def close(
799804

800805
await self.session.execute(
801806
update(TaskingProject)
802-
.where(TaskingProject.id == project.id)
807+
.where(
808+
TaskingProject.id == project.id # pyright: ignore[reportArgumentType]
809+
)
803810
.values(status=ProjectStatus.DONE, updated_at=datetime.now())
804811
)
805812
await self._audit(
@@ -847,7 +854,10 @@ async def reset(
847854
if project.status == ProjectStatus.DONE:
848855
await self.session.execute(
849856
update(TaskingProject)
850-
.where(TaskingProject.id == project.id)
857+
.where(
858+
TaskingProject.id
859+
== project.id # pyright: ignore[reportArgumentType]
860+
)
851861
.values(status=ProjectStatus.OPEN, updated_at=datetime.now())
852862
)
853863

@@ -870,7 +880,7 @@ async def get_aoi(self, workspace_id: int, project_id: int) -> AoiFeature:
870880
geom = to_shape(project.aoi)
871881
if isinstance(geom, ShapelyPolygon): # defensive
872882
geom = ShapelyMultiPolygon([geom])
873-
return _shapely_to_aoi_feature(geom)
883+
return _shapely_to_aoi_feature(geom) # pyright: ignore[reportArgumentType]
874884

875885
async def upload_aoi(
876886
self, workspace_id: int, project_id: int, aoi: AoiInput, current_user: UserInfo
@@ -893,7 +903,9 @@ async def upload_aoi(
893903
)
894904
await self.session.execute(
895905
update(TaskingProject)
896-
.where(TaskingProject.id == project.id)
906+
.where(
907+
TaskingProject.id == project.id # pyright: ignore[reportArgumentType]
908+
)
897909
.values(
898910
aoi=from_shape(geom, srid=4326),
899911
task_boundary_type=None,
@@ -1355,7 +1367,9 @@ async def delete_aoi(
13551367
)
13561368
await self.session.execute(
13571369
update(TaskingProject)
1358-
.where(TaskingProject.id == project.id)
1370+
.where(
1371+
TaskingProject.id == project.id # pyright: ignore[reportArgumentType]
1372+
)
13591373
.values(
13601374
aoi=None,
13611375
task_boundary_type=None,

api/src/tasking/tasks/repository.py

Lines changed: 52 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,3 @@
1-
# SQLModel declares columns as plain annotations (e.g. ``id: int | None``) rather
2-
# than ``Mapped[int]``, so Pyright reads ``Column == value`` as ``bool`` and
3-
# misjudges ``where()``/``select()``/``selectinload`` calls; nullable columns
4-
# (``deleted_at``, ``released_at``) additionally trip ``reportOptionalMemberAccess``
5-
# on ``.is_(None)``. These are framework false positives; the queries are valid at
6-
# runtime. Other rules stay enabled so real bugs still surface.
7-
# pyright: reportArgumentType=false, reportCallIssue=false, reportAttributeAccessIssue=false, reportOptionalMemberAccess=false
8-
91
from __future__ import annotations
102

113
import hashlib
@@ -179,7 +171,11 @@ def _generate_grid_over_aoi(
179171
# `intersection` can return a Polygon, MultiPolygon,
180172
# or GeometryCollection; retain polygon pieces only.
181173
geoms = (
182-
list(clipped.geoms) if hasattr(clipped, "geoms") else [clipped]
174+
list(
175+
clipped.geoms # pyright: ignore[reportAttributeAccessIssue]
176+
)
177+
if hasattr(clipped, "geoms")
178+
else [clipped]
183179
)
184180
for piece in geoms:
185181
if isinstance(piece, ShapelyPolygon) and piece.area > 0:
@@ -215,7 +211,11 @@ async def _get_project(self, workspace_id: int, project_id: int) -> TaskingProje
215211
select(TaskingProject).where(
216212
(TaskingProject.id == project_id)
217213
& (TaskingProject.workspace_id == workspace_id)
218-
& (TaskingProject.deleted_at.is_(None))
214+
& (
215+
TaskingProject.deleted_at.is_( # pyright: ignore[reportAttributeAccessIssue, reportOptionalMemberAccess]
216+
None
217+
)
218+
)
219219
)
220220
)
221221
project = rs.scalar_one_or_none()
@@ -226,7 +226,9 @@ async def _get_project(self, workspace_id: int, project_id: int) -> TaskingProje
226226
async def _get_task(self, project_id: int, task_number: int) -> TaskingTask:
227227
rs = await self.session.execute(
228228
select(TaskingTask).where(
229-
(TaskingTask.project_id == project_id)
229+
(
230+
TaskingTask.project_id == project_id
231+
) # pyright: ignore[reportArgumentType]
230232
& (TaskingTask.task_number == task_number)
231233
)
232234
)
@@ -240,7 +242,12 @@ async def _get_task(self, project_id: int, task_number: int) -> TaskingTask:
240242
async def _get_active_lock(self, task_id: int) -> Optional[TaskingLock]:
241243
rs = await self.session.execute(
242244
select(TaskingLock).where(
243-
(TaskingLock.task_id == task_id) & (TaskingLock.released_at.is_(None))
245+
(TaskingLock.task_id == task_id)
246+
& (
247+
TaskingLock.released_at.is_( # pyright: ignore[reportAttributeAccessIssue, reportOptionalMemberAccess]
248+
None
249+
)
250+
)
244251
)
245252
)
246253
return rs.scalar_one_or_none()
@@ -252,7 +259,11 @@ async def _get_active_lock_for_user_in_project(
252259
select(TaskingLock).where(
253260
(TaskingLock.project_id == project_id)
254261
& (TaskingLock.user_auth_uid == user_auth_uid)
255-
& (TaskingLock.released_at.is_(None))
262+
& (
263+
TaskingLock.released_at.is_( # pyright: ignore[reportAttributeAccessIssue, reportOptionalMemberAccess]
264+
None
265+
)
266+
)
256267
)
257268
)
258269
return rs.scalar_one_or_none()
@@ -390,7 +401,9 @@ async def generate_grid(
390401
if isinstance(aoi_geom, ShapelyPolygon):
391402
aoi_geom = ShapelyMultiPolygon([aoi_geom])
392403

393-
cells = _generate_grid_over_aoi(aoi_geom, float(cell_size_m))
404+
cells = _generate_grid_over_aoi(
405+
aoi_geom, float(cell_size_m) # pyright: ignore[reportArgumentType]
406+
)
394407
if not cells:
395408
raise HTTPException(
396409
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
@@ -537,7 +550,9 @@ async def save(
537550
task = TaskingTask(
538551
project_id=project.id, # type: ignore[arg-type]
539552
task_number=idx + 1,
540-
area_sqkm=round(_polygon_area_km2(poly), 4),
553+
area_sqkm=round(
554+
_polygon_area_km2(poly), 4
555+
), # pyright: ignore[reportArgumentType]
541556
status=TaskStatus.TO_MAP,
542557
geometry=from_shape(poly, srid=4326),
543558
)
@@ -549,7 +564,9 @@ async def save(
549564
# Set boundary type + bump project updated_at.
550565
await self.session.execute(
551566
update(TaskingProject)
552-
.where(TaskingProject.id == project.id)
567+
.where(
568+
TaskingProject.id == project.id # pyright: ignore[reportArgumentType]
569+
)
553570
.values(
554571
task_boundary_type=body.source,
555572
updated_at=datetime.now(),
@@ -616,7 +633,9 @@ async def list_tasks(
616633
where = where & (TaskingTask.last_mapper_id == str(last_mapper_id))
617634

618635
total_q = await self.session.execute(
619-
select(func.count()).select_from(TaskingTask).where(where)
636+
select(func.count())
637+
.select_from(TaskingTask)
638+
.where(where) # pyright: ignore[reportArgumentType]
620639
)
621640
total = int(total_q.scalar() or 0)
622641

@@ -626,8 +645,10 @@ async def list_tasks(
626645

627646
rows = await self.session.execute(
628647
select(TaskingTask)
629-
.where(where)
630-
.order_by(TaskingTask.task_number.asc())
648+
.where(where) # pyright: ignore[reportArgumentType]
649+
.order_by(
650+
TaskingTask.task_number.asc() # pyright: ignore[reportAttributeAccessIssue]
651+
)
631652
.limit(page_size)
632653
.offset(offset)
633654
)
@@ -727,7 +748,10 @@ async def lock_task(
727748
)
728749
if other is not None:
729750
other_task_rs = await self.session.execute(
730-
select(TaskingTask).where(TaskingTask.id == other.task_id)
751+
select(TaskingTask).where(
752+
TaskingTask.id
753+
== other.task_id # pyright: ignore[reportArgumentType]
754+
)
731755
)
732756
other_task = other_task_rs.scalar_one()
733757
summary = ExistingLockSummary(
@@ -803,7 +827,7 @@ async def unlock_task(
803827
now = datetime.now()
804828
await self.session.execute(
805829
update(TaskingLock)
806-
.where(TaskingLock.id == lock.id)
830+
.where(TaskingLock.id == lock.id) # pyright: ignore[reportArgumentType]
807831
.values(released_at=now, release_reason=release_reason)
808832
)
809833
await self._audit(
@@ -846,7 +870,7 @@ async def extend_lock(
846870
new_expiry = prev + timedelta(hours=project.lock_timeout_hours)
847871
await self.session.execute(
848872
update(TaskingLock)
849-
.where(TaskingLock.id == lock.id)
873+
.where(TaskingLock.id == lock.id) # pyright: ignore[reportArgumentType]
850874
.values(expires_at=new_expiry)
851875
)
852876
await self._audit(
@@ -884,7 +908,7 @@ async def reset_task(
884908
if lock is not None:
885909
await self.session.execute(
886910
update(TaskingLock)
887-
.where(TaskingLock.id == lock.id)
911+
.where(TaskingLock.id == lock.id) # pyright: ignore[reportArgumentType]
888912
.values(
889913
released_at=now,
890914
release_reason=LockReleaseReason.RESET,
@@ -905,7 +929,7 @@ async def reset_task(
905929
if previous_status != TaskStatus.TO_MAP:
906930
await self.session.execute(
907931
update(TaskingTask)
908-
.where(TaskingTask.id == task.id)
932+
.where(TaskingTask.id == task.id) # pyright: ignore[reportArgumentType]
909933
.values(
910934
status=TaskStatus.TO_MAP,
911935
last_mapper_id=None,
@@ -927,7 +951,7 @@ async def reset_task(
927951
# Clear last_mapper_id even if state was already to_map.
928952
await self.session.execute(
929953
update(TaskingTask)
930-
.where(TaskingTask.id == task.id)
954+
.where(TaskingTask.id == task.id) # pyright: ignore[reportArgumentType]
931955
.values(last_mapper_id=None, updated_at=now)
932956
)
933957

@@ -1009,7 +1033,7 @@ async def submit(
10091033
new_expiry = now + timedelta(hours=project.lock_timeout_hours)
10101034
await self.session.execute(
10111035
update(TaskingLock)
1012-
.where(TaskingLock.id == lock.id)
1036+
.where(TaskingLock.id == lock.id) # pyright: ignore[reportArgumentType]
10131037
.values(expires_at=new_expiry)
10141038
)
10151039
await self._audit(
@@ -1075,7 +1099,7 @@ async def submit(
10751099
# Release the lock (auto_unlock).
10761100
await self.session.execute(
10771101
update(TaskingLock)
1078-
.where(TaskingLock.id == lock.id)
1102+
.where(TaskingLock.id == lock.id) # pyright: ignore[reportArgumentType]
10791103
.values(
10801104
released_at=now,
10811105
release_reason=LockReleaseReason.AUTO_UNLOCK,
@@ -1095,7 +1119,7 @@ async def submit(
10951119
# Apply state transition.
10961120
await self.session.execute(
10971121
update(TaskingTask)
1098-
.where(TaskingTask.id == task.id)
1122+
.where(TaskingTask.id == task.id) # pyright: ignore[reportArgumentType]
10991123
.values(
11001124
status=new_status,
11011125
last_mapper_id=new_last_mapper,

0 commit comments

Comments
 (0)