chore: make ty check blocking with targeted suppressions

Co-authored-by: Copilot App <[email protected]>
This commit is contained in:
Jim Lancaster
2026-08-23 18:17:44 -05:00
co-authored by Copilot App
parent a2bb1acd6b
commit 26f9c83f54
4 changed files with 40 additions and 12 deletions
+4 -5
View File
@@ -1,6 +1,5 @@
# Quality gate for V4.6 [HIGH-06]. `ruff check` is blocking. `ty check` is advisory # Quality gate for V4.6 [HIGH-06]. `ruff check`, `ruff format --check`, and `ty check`
# during release stabilization: it reports its whole-project baseline without failing # are blocking once known `ty` false positives are suppressed inline with rationale.
# the commit. Restore it to blocking once that baseline is clear.
# #
# Both tools are uv-managed dev dependencies and are not on PATH, so each entry must # Both tools are uv-managed dev dependencies and are not on PATH, so each entry must
# go through `uv run`. # go through `uv run`.
@@ -21,8 +20,8 @@ repos:
pass_filenames: false pass_filenames: false
require_serial: true require_serial: true
- id: ty - id: ty
name: ty check (advisory) name: ty check
entry: python -c "import subprocess, sys; subprocess.run(['uv', 'run', 'ty', 'check']); sys.exit(0)" entry: uv run ty check
language: system language: system
types_or: [python, pyi] types_or: [python, pyi]
pass_filenames: false pass_filenames: false
+14
View File
@@ -113,3 +113,17 @@ Declared with `>=` floors and moved by explicit `uv lock --upgrade`. Verify with
`uv run ruff check .`, `uv run ty check`, and `uv run pytest -q -m "not external"` `uv run ruff check .`, `uv run ty check`, and `uv run pytest -q -m "not external"`
before committing a changed lockfile. before committing a changed lockfile.
## 7. Type-check suppression policy
`uv run ty check` is a blocking pre-commit gate. Suppressions are allowed only for
proven SQLAlchemy descriptor false positives where runtime behavior is correct and
the checker cannot represent the descriptor protocol at that call site.
Every suppression must be:
1. **Targeted** to a single rule (for example `# ty: ignore[unresolved-attribute]`).
2. **Inline** on the expression it suppresses (not file-wide).
3. Followed by a **one-line rationale** stating it is a SQLAlchemy descriptor false positive.
Do not use broad or rationale-free suppressions. If a diagnostic is not a known
false positive, fix the code instead of suppressing it.
+14 -6
View File
@@ -178,25 +178,33 @@ class PhotosService(ServiceBase):
async def _list_owner_photos(self, *, session: AsyncSession, person_id: UUID | None) -> list[Photo]: async def _list_owner_photos(self, *, session: AsyncSession, person_id: UUID | None) -> list[Photo]:
query = select(Photo) query = select(Photo)
if person_id is None: if person_id is None:
query = query.where(Photo.person_id.is_(None)) query = query.where(Photo.person_id.is_(None)) # ty: ignore[unresolved-attribute] - SQLAlchemy descriptor false positive.
else: else:
query = query.where(Photo.person_id == person_id) query = query.where(Photo.person_id == person_id)
query = query.order_by(Photo.created_at.asc(), Photo.id.asc()) query = query.order_by(
Photo.created_at.asc(), # ty: ignore[unresolved-attribute] - SQLAlchemy descriptor false positive.
Photo.id.asc(), # ty: ignore[unresolved-attribute] - SQLAlchemy descriptor false positive.
)
return list((await session.exec(query)).all()) return list((await session.exec(query)).all())
async def _owner_oldest_photo(self, *, session: AsyncSession, person_id: UUID | None) -> Photo | None: async def _owner_oldest_photo(self, *, session: AsyncSession, person_id: UUID | None) -> Photo | None:
query = select(Photo) query = select(Photo)
if person_id is None: if person_id is None:
query = query.where(Photo.person_id.is_(None)) query = query.where(Photo.person_id.is_(None)) # ty: ignore[unresolved-attribute] - SQLAlchemy descriptor false positive.
else: else:
query = query.where(Photo.person_id == person_id) query = query.where(Photo.person_id == person_id)
query = query.order_by(Photo.created_at.asc(), Photo.id.asc()).limit(1) query = query.order_by(
Photo.created_at.asc(), # ty: ignore[unresolved-attribute] - SQLAlchemy descriptor false positive.
Photo.id.asc(), # ty: ignore[unresolved-attribute] - SQLAlchemy descriptor false positive.
).limit(1)
return (await session.exec(query)).first() return (await session.exec(query)).first()
async def _clear_owner_primary(self, *, session: AsyncSession, person_id: UUID | None) -> None: async def _clear_owner_primary(self, *, session: AsyncSession, person_id: UUID | None) -> None:
query = select(Photo).where(Photo.is_primary.is_(True)) query = select(Photo).where(
Photo.is_primary.is_(True) # ty: ignore[unresolved-attribute] - SQLAlchemy descriptor false positive.
)
if person_id is None: if person_id is None:
query = query.where(Photo.person_id.is_(None)) query = query.where(Photo.person_id.is_(None)) # ty: ignore[unresolved-attribute] - SQLAlchemy descriptor false positive.
else: else:
query = query.where(Photo.person_id == person_id) query = query.where(Photo.person_id == person_id)
for current in (await session.exec(query)).all(): for current in (await session.exec(query)).all():
+8 -1
View File
@@ -35,7 +35,14 @@ async def _document_ids(settings: Settings) -> set[str]:
async def _source_counts_by_document(settings: Settings) -> dict[str, int]: async def _source_counts_by_document(settings: Settings) -> dict[str, int]:
async with session_scope(settings=settings) as session: async with session_scope(settings=settings) as session:
rows = await session.exec(select(Source.document_id, func.count(Source.id)).group_by(Source.document_id)) rows = await session.exec(
select(
Source.document_id,
func.count(Source.id), # ty: ignore[invalid-argument-type] - SQLAlchemy descriptor false positive.
).group_by(
Source.document_id # ty: ignore[invalid-argument-type] - SQLAlchemy descriptor false positive.
)
)
return {str(document_id): int(count) for document_id, count in rows} return {str(document_id): int(count) for document_id, count in rows}