typing: graduate opencontractserver.documents.* (refs #1447)#1475
typing: graduate opencontractserver.documents.* (refs #1447)#1475
Conversation
Continues draining the mypy baseline. Graduates all eight documents.*
modules.
Removed from mypy.ini
- opencontractserver.documents.{admin,checks,models,query_optimizer,
versioning}
- opencontractserver.documents.tests.test_pdf_hash
- opencontractserver.documents.management.commands.{
compute_document_hashes,migrate_pipeline_settings}
Added to mypy.ini
- [mypy-opencontractserver.documents.models] disable_error_code =
django-manager-missing (DocumentPath self-relation path_records /
children and IngestionSource.document_paths are django-tree-queries
reverse-relation surfaces django-stubs cannot resolve)
Pruned 40 baseline lines (7124 -> 7084).
Per-file fixes
- versioning.py: replaced runtime User = get_user_model() with
TYPE_CHECKING import of opencontractserver.users.models.User and
quoted six `user: "User"` annotations; widened
move_document(new_folder=...) from Optional[CorpusFolder] = "UNSET"
to Optional[CorpusFolder] | str = "UNSET" and added
`assert not isinstance(new_folder, str)` after the sentinel check so
folder_to_use: Optional[CorpusFolder] narrows correctly.
- models.py: scoped # type: ignore[misc] on the
`objects = DocumentManager()` manager override; added
`author_obj: AbstractBaseUser` annotation in Document.update_summary
plus # type: ignore[misc] on the corresponding author=author_obj
kwarg passed to DocumentSummaryRevision.objects.create(); typed two
`all_components: list[Any] = []` accumulators.
- query_optimizer.py: typed `result: dict[str, list[Any]] = {...}` in
the related-object collector.
- checks.py: typed three `errors: list[Warning] = []` accumulators
(one widened to list[Error | Warning] because the body appends both
severity classes).
- admin.py: scoped # type: ignore[attr-defined] on the three
Django-admin `method.short_description = "..."` /
`admin_order_field = "..."` annotations.
- tests/test_pdf_hash.py: added narrowing
`assert computed_hash is not None` before the SHA-256 length
assertion; cast `_meta.get_field("pdf_file_hash")` to Any in the
field-introspection helper.
- management/commands/compute_document_hashes.py: added
`assert document.pdf_file.name is not None` before
storage.exists(); coalesced `old_hash[:8]` to `(old_hash or "")[:8]`
for the no-change branch.
- management/commands/migrate_pipeline_settings.py: typed two
`all_components: list[Any] = []` accumulators; widened the
heterogeneous stats dict to dict[str, Any]; renamed the inner
iteration variable to group_components and rebound
`components: list[Any] = list(group_components)` so the list-comp
re-assignment doesn't conflict with the source tuple type.
Verification
- pre-commit run --all-files -> all hooks pass (mypy: Passed)
- docker compose -f test.yml run --rm django python -m mypy
--config-file mypy.ini opencontractserver config -> "no issues found
in 1019 source files"
Code Review: typing: graduate
|
| Correctness | ✅ No runtime behavior changes; (old_hash or '')[:8] fixes a latent bug |
| Type safety | assert vs raise for sentinel narrowing; str too broad vs Literal["UNSET"] |
| Conventions | |
| Tests | ✅ Changes are appropriate; no new tests needed for a typing-only PR |
| Security | ✅ No concerns |
The two sentinel-related points (items 1 and 2) are the ones most worth addressing before merge. Items 3–5 are lower priority but worth a quick pass.
Resolves three conflicts (CHANGELOG.md, docs/typing/mypy_baseline.txt, mypy.ini): documents.* modules graduated by this PR are kept off the ignore list and out of the baseline, while origin/main's parallel typing graduations (corpuses, agents, analyzer, llms.tools, shared, small single-file packages) are preserved.
The PR commit message claimed to quote six `user: "User"` annotations but two were missed: `permanently_delete_document` (line 677) and `permanently_delete_all_in_trash` (line 782). Without quotes those annotations are evaluated at function-definition time, so importing `opencontractserver.documents.versioning` raised `NameError: name 'User' is not defined` because `User` is only imported under `TYPE_CHECKING`. That broke every test module transitively importing versioning (sidecar/zip imports, document_path migrations, agent memory curate task, etc.) — which is what the failing pytest run reported. Quote both annotations to match the existing pattern (lines 136, 413, 469, 506); now all six `user: "User"` occurrences are consistent.
Addresses the five points from claude[bot]'s review on the original
(pre-merge) revision of this PR:
1. versioning.py move_document() — replace `assert not isinstance(...)`
with a real `raise TypeError` so the sentinel guard is not silently
stripped when Python runs with `-O`.
2. versioning.py move_document() — tighten `new_folder` annotation
from `Optional[CorpusFolder] | str` to
`Optional[CorpusFolder] | Literal['UNSET']` so callers passing any
other string fail at type-check time rather than runtime.
3. Trim explanatory comments that restated the type-narrowing mechanics
for mypy rather than documenting non-obvious invariants:
- migrate_pipeline_settings.py:606 (the list(...) widening blurb)
- compute_document_hashes.py:74-77 (the str-or-None recap above the
narrowing assert)
4. checks.py — widen the three `errors: list[Warning]` accumulators
to `list[Error | Warning]` so all four check functions in the
module use a uniform error-list type. Future check additions can
append `Error` items without re-typing the accumulator.
5. tests/test_pdf_hash.py — move the `assert computed_hash is not None`
narrowing line above the first `assertEqual(computed_hash, ...)`
call so the intent (narrow before use) reads top-to-bottom.
Code Review: typing: graduate
|
The merge with origin/main accidentally retained ignore_errors blocks for modules that origin/main had since graduated (corpuses.*, discovery.views, examples.*, feedback.models). This also produced a duplicate [mypy-opencontractserver.discovery.tests.test_discovery_views] section, which made mypy abort config parsing and surface ~2.1k baseline errors. Removes the redundant blocks. The narrower User typing in versioning.py also broke a corpuses.models call site that passes AbstractBaseUser into import_document; suppress with a localized type: ignore[arg-type] rather than re-baselining corpuses.models. Address Claude review (PR #1475): replace bare 'assert pdf_file.name is not None' in compute_document_hashes with an explicit None-guard that records an error and continues, matching the surrounding error-handling style and surviving 'python -O'.
Code ReviewSummary: Typing graduation for Genuine runtime changes (the PR description says "no runtime behavior changes intended")
elif isinstance(new_folder, str):
raise TypeError(...)Previously, a stray string (e.g.,
|
Continues draining the mypy baseline. Graduates all eight `opencontractserver.documents.*` modules.
Removed from `mypy.ini`
Added to `mypy.ini`
```
[mypy-opencontractserver.documents.models]
disable_error_code = django-manager-missing
```
`DocumentPath` self-relation `path_records` / `children` and `IngestionSource.document_paths` are django-tree-queries reverse-relation surfaces that django-stubs cannot resolve.
Pruned from `docs/typing/mypy_baseline.txt`
40 lines (7124 → 7084).
Per-file fixes (highlights)
Acceptance (per #1447)
```
$ docker compose -f test.yml run --rm django python -m mypy --config-file mypy.ini opencontractserver config
Success: no issues found in 1019 source files
$ pre-commit run --all-files
mypy.....................................................................Passed
```
Test plan