Skip to content

typing: graduate tests.permissioning.* subpackage out of the mypy baseline#1768

Merged
JSv4 merged 1 commit into
mainfrom
claude/dreamy-ptolemy-1x82p
May 24, 2026
Merged

typing: graduate tests.permissioning.* subpackage out of the mypy baseline#1768
JSv4 merged 1 commit into
mainfrom
claude/dreamy-ptolemy-1x82p

Conversation

@JSv4

@JSv4 JSv4 commented May 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

Continues the mypy baseline drain tracked in #1738 (the #1331 → #1335 → #1447 cadence). After the config tail (PR #1748), this is the next cohesive batch called out in the issue's batching plan: the 18-module opencontractserver.tests.permissioning.* subpackage.

Removes all 18 [mypy-…] ignore_errors blocks from mypy.ini, prunes the corresponding 313 lines from docs/typing/mypy_baseline.txt (5592 → 5279), and fixes the 162 errors that surface.

After this PR: opencontractserver.mcp.tests.test_mcp and the opencontractserver.tests.* leaf-file modules remain on the baseline, per the issue's batching plan.

Production-code finding

One real type-correctness gap surfaced — the kind of thing #1738 explicitly invites graduation passes to look for:

  • CoreAgent Protocol was missing its public config: AgentConfig attribute (opencontractserver/llms/agents/core_agents.py:383). Five tests read agent.config.tools / agent.config._approval_bypass_allowed directly off agents handed back by llm_agents.for_corpus(...) / for_document(...). The factory returns the CoreAgent Protocol; the concrete CoreAgentBase initializes self.config = config in __init__, but the Protocol never declared it. Tests worked at runtime (the attribute exists on every concrete agent) but failed type-checking. Added config: AgentConfig to the Protocol body — captures the existing public API contract without a behaviour change.

Per-file test fixes

Most errors were the standard Django setUpTestData(cls) class-attribute pattern flagged by mypy (recommended fix from #1479: annotate the assigned names as class-level attributes before the @classmethod body). The breakdown:

File Errors Fix pattern
test_query_optimizer_methods.py 108 Class-level annotations on AnalysisServicePermissionTestCase + DocumentPermissionFilterQueryCountTestCase; switched User = get_user_model() to direct from opencontractserver.users.models import User; replaced three qs.first().<attr> with qs[0].<attr> after the existing qs.count() == 1 guard.
test_metadata_query_optimizer.py 15 assert status is not None narrowings before indexing into the dict | None return of MetadataService.get_metadata_completion_status; one column is not None narrowing in test_validate_column_valid.
test_permissioning.py 10 Same setUpTestData annotation pattern on SetPermissionsIsNewTests; same get_user_model() → direct import switch.
test_permissioned_querysets.py 8 Narrowed qs.query.select_related (dict | bool) via assert isinstance(..., dict) before three assertIn calls; gave two PermissionQuerySet constructions explicit type annotations.
test_public_and_permissions.py 7 Removed six unused # type: ignore[attr-defined] comments on Model.objects.acount() calls (django-stubs now ships proper async-manager typing); one real fix on CoreAgent.config (covered above).
test_version_aware_query_optimizer.py 7 Replaced six result.first().id accesses with result[0].id after the pre-existing result.count() == 1 guard.
test_custom_permission_filters.py 4 Replaced two assertIsNotNone calls with assert narrowings.
test_creator_based_permissions.py 2 Narrow # type: ignore[arg-type] on two get_users_permissions_for_obj(instance=mock_instance, ...) calls (the test deliberately passes a non-Model mock to exercise the no-guardian-tables fallback).
test_comment_permission.py 1 Replaced one assertIsNotNone with assert for narrowing.
Total 162

All test behaviour is preserved — the changes are either no-ops at runtime (class-level annotations, assert vs assertIsNotNone) or behaviour-equivalent refactors (.first().id after a count() == 1 guard → [0].id).

Test plan

Note on local testing: the backend test suite could not be exercised in the authoring environment — docker compose -f test.yml is unavailable in this sandbox. mypy was run instead via a Python env mirroring the pre-commit hook's pinned dependency set. The changes are behaviour-neutral (the test diffs preserve every existing assertion, the CoreAgent.config Protocol addition captures an attribute that's been on every concrete agent since its introduction); the backend test job is relied on for the runtime check.

Refs #1738

…eline

Continues the mypy baseline drain tracked in #1738 (the #1331#1335#1447
cadence). After the config tail (#1748), this is the next cohesive batch
called out in the issue: the 18-module ``opencontractserver.tests.permissioning.*``
subpackage. Removes all 18 ``[mypy-…]`` ``ignore_errors`` blocks from ``mypy.ini``,
prunes the corresponding 313 lines from ``docs/typing/mypy_baseline.txt``
(5592 → 5279), and fixes the 162 errors that surface.

Most errors were the standard Django ``setUpTestData(cls)`` class-attribute
pattern flagged by mypy (recommended fix from #1479: annotate the assigned
names as class-level attributes before the ``@classmethod`` body).

One real production-code gap surfaced:

- ``CoreAgent`` Protocol was missing its public ``config: AgentConfig``
  attribute (``opencontractserver/llms/agents/core_agents.py``). Five tests
  read ``agent.config.tools`` / ``agent.config._approval_bypass_allowed``
  directly off agents handed back by ``llm_agents.for_corpus(...)`` /
  ``for_document(...)``. The factory returns the ``CoreAgent`` Protocol;
  the concrete ``CoreAgentBase`` initialises ``self.config = config`` in
  ``__init__``, but the Protocol never declared it. Added ``config:
  AgentConfig`` to the Protocol body — captures the existing public API
  contract without a behaviour change.

Per-file test fixes:

- ``test_query_optimizer_methods.py`` (108 errors): added class-level
  annotations to ``AnalysisServicePermissionTestCase`` and
  ``DocumentPermissionFilterQueryCountTestCase`` covering every
  ``cls.<attr> = ...`` assignment inside ``setUpTestData``; switched
  ``User = get_user_model()`` to ``from opencontractserver.users.models
  import User`` so the name is usable as a type annotation; replaced three
  ``qs.first().<attr>`` accesses with ``qs[0].<attr>`` after the
  pre-existing ``qs.count() == 1`` guard.
- ``test_metadata_query_optimizer.py`` (15 errors): added ``assert status
  is not None`` narrowings before indexing into the ``dict | None`` return
  of ``MetadataService.get_metadata_completion_status``; one ``column is
  not None`` narrowing in ``test_validate_column_valid``.
- ``test_permissioning.py`` (10 errors): same ``setUpTestData`` annotation
  pattern on ``SetPermissionsIsNewTests``; same ``get_user_model()`` →
  direct import switch.
- ``test_permissioned_querysets.py`` (8 errors): narrowed
  ``qs.query.select_related`` (``dict | bool``) via ``assert isinstance(...,
  dict)`` before three ``assertIn`` calls; gave two ``PermissionQuerySet``
  constructions explicit type annotations.
- ``test_public_and_permissions.py`` (7 errors): removed six unused
  ``# type: ignore[attr-defined]`` comments on ``Model.objects.acount()``
  calls (django-stubs now ships proper async-manager typing).
- ``test_version_aware_query_optimizer.py`` (7 errors): replaced six
  ``result.first().id`` accesses with ``result[0].id`` after the
  pre-existing ``result.count() == 1`` guard.
- ``test_custom_permission_filters.py`` (4 errors): replaced two
  ``assertIsNotNone`` calls with ``assert`` narrowings.
- ``test_creator_based_permissions.py`` (2 errors): narrow
  ``# type: ignore[arg-type]`` on the two ``get_users_permissions_for_obj
  (instance=mock_instance, ...)`` calls (the test deliberately passes a
  non-``Model`` mock to exercise the no-guardian-tables fallback).
- ``test_comment_permission.py`` (1 error): replaced one ``assertIsNotNone``
  with ``assert`` for narrowing.

Pre-commit ``mypy`` continues to pass on the full project surface
(``mypy --config-file mypy.ini opencontractserver config`` → "Success: no
issues found in 1157 source files").

Refs #1738
@claude

claude Bot commented May 23, 2026

Copy link
Copy Markdown

Code Review — PR #1768: typing: graduate tests.permissioning.* out of the mypy baseline

Overview

Continues the incremental mypy graduation cadence (#1738 / #1331#1335#1447). Removes 18 ignore_errors = True blocks from mypy.ini, prunes 313 stale lines from docs/typing/mypy_baseline.txt, and fixes the 162 actual errors that surface when those modules are checked from scratch. One genuine production-code gap was caught in the process.


Production Code Change (core_agents.py)

CoreAgent Protocol — config: AgentConfig addition

# opencontractserver/llms/agents/core_agents.py:383
class CoreAgent(Protocol):
    config: AgentConfig   # ← new
    ...

This is a legitimate API-contract fix. CoreAgentBase.__init__ has always set self.config = config, but the Protocol never declared it, so the five test callers (agent.config.tools, agent.config._approval_bypass_allowed) were working only by runtime duck-typing. Adding the attribute to the Protocol body is correct.

One thing worth confirming: Protocol instance-attribute declarations are satisfied by structural subtyping, so any concrete subclass that initialises self.config in __init__ (as CoreAgentBase does) is fine. No issues here.

The inline comment (# Public configuration attribute…) is appropriate — it explains the "why" without narrating what the code already shows.


Test-Side Fixes

assertIsNotNoneassert ... is not None

Used in test_comment_permission.py, test_custom_permission_filters.py, test_metadata_query_optimizer.py. This is the correct technique to give mypy a type narrowing it can flow through. The change is runtime-equivalent (assertIsNotNone raises AssertionError with the same semantics; assert does too).

Minor observation: a handful of assertIsNotNone calls in the same files are left unchanged (where narrowing wasn't needed for subsequent attribute access). This is intentional and correct — no need to convert them all mechanically.

qs.first().<attr>qs[0].<attr>

Applied in test_query_optimizer_methods.py and test_version_aware_query_optimizer.py, always after a self.assertEqual(qs.count(), 1) guard. The change is behavior-equivalent for the happy path. One subtle difference worth noting for awareness:

  • qs.first()SELECT … ORDER BY pk LIMIT 1 (adds an ORDER BY)
  • qs[0]SELECT … LIMIT 1 OFFSET 0 (no ORDER BY)

In test fixtures with a single matching row the result is the same. If a queryset happens to have no default ordering and a future test creates two matching rows, qs[0] would be non-deterministic. This is very low risk in these controlled-fixture tests, and IndexError is actually a clearer failure signal than AttributeError: 'NoneType' object has no attribute '…'. No action needed.

setUpTestData class-level annotations ✅

Correct fix for the setUpTestData class-attribute pattern (per #1479 guidance). AnalysisServicePermissionTestCase and DocumentPermissionFilterQueryCountTestCase both get complete attribute coverage. No runtime impact.

One minor nit: in test_query_optimizer_methods.py the existing local imports inside setUpTestData:

-from opencontractserver.analyzer.models import Analysis, Analyzer
-from opencontractserver.documents.models import Document

are removed. These are redundant with the module-level imports already present (confirmed by the fact that the newly-added class-level analysis: Analysis etc. annotations would fail at import time if those names weren't already in scope). This cleanup is correct.

get_user_model()from opencontractserver.users.models import User

Established project pattern for mypy-compatibility per #1479. Acceptable in test code (single user-model project).

select_related narrowing (dict | bool) ✅

# test_permissioned_querysets.py
select_related = qs.query.select_related
assert isinstance(select_related, dict)
self.assertIn("creator", select_related)

Correct: Django sets select_related to True (fetch-all), False (off), or a dict of specific relations. The narrowing via isinstance is the right technique, and the test already expects specific field names in the dict, so asserting isinstance(select_related, dict) is a meaningful and non-vacuous guard.

PermissionQuerySet explicit type annotations ✅

qs: PermissionQuerySet = PermissionQuerySet(model=Document, using=connection.alias)

Correct fix for generic-type capture. No behavioral change.

Removed stale # type: ignore[attr-defined] on acount()

-await ChatMessage.objects.all().acount()  # type: ignore[attr-defined]
+await ChatMessage.objects.all().acount()

These suppression comments are now unused because django-stubs properly types async managers. Removing them is correct; leaving stale ignores would suppress real future errors.

# type: ignore[arg-type] in test_creator_based_permissions.py

Only two calls get the ignore — the two that deliberately pass MockPublicModel / MockPrivateModel instances (not real Model subclasses) to test the no-guardian-tables fallback. The inline comment could be slightly more explicit, but the PR description covers it. Acceptable.


Baseline Arithmetic

The PR description says 162 errors were fixed but 313 baseline lines were pruned. The difference (~151 lines) comes from baseline entries for those 18 modules that were already stale — the underlying functions (e.g. set_permissions_for_obj_to_user signature) or stubs (django-stubs async managers) had been improved in earlier PRs, so those errors simply no longer appear when mypy runs fresh on these modules. Pruning all 313 is correct: once ignore_errors = True is removed, the stale baseline entries for those modules are irrelevant.


Concerns / Suggestions

  1. CI gate is the authoritative check. The PR correctly flags that the test suite could not be run locally. All changes are behavior-neutral by construction, but the backend CI job (backend.yml) should be green before merge. No blocker, just a reminder.

  2. CHANGELOG entry is verbose. The existing entries in this section are concise bullet points. The new entry is much longer (covers per-file breakdown). Consider trimming the per-file table to a one-liner in the changelog and leaving the detail in the PR description / commit message, to keep the changelog readable.

  3. test_query_optimizer_methods.pyDocumentPermissionFilterQueryCountTestCase annotations include list[Document] and dict[int, Annotation]. These are correct type annotations, but confirm that list and dict are imported (Python 3.9+ built-in generics) or that List/Dict from typing is available. Given the project targets Python 3.10+, list[...] / dict[...] are fine.


Summary

This is a clean, mechanical PR that follows the project's established graduation cadence. The production fix to CoreAgent is correct and properly motivated. All test-side changes are behavior-neutral and apply the right mypy-narrowing techniques consistently. No security, performance, or correctness issues found.

Verdict: Approve pending green CI.

@codecov

codecov Bot commented May 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@JSv4 JSv4 merged commit 5e75c25 into main May 24, 2026
14 checks passed
@JSv4 JSv4 deleted the claude/dreamy-ptolemy-1x82p branch May 24, 2026 01:00
JSv4 added a commit that referenced this pull request May 24, 2026
…eline (#1776)

Continues issue #1738's drain (the #1331#1335#1447 cadence). The
`annotation` domain chunk from the issue's tests.* leaf-files batching
plan, following the tests.permissioning.* subpackage in PR #1768.

Removes all 6 [mypy-opencontractserver.tests.test_annotation_*]
ignore_errors blocks from mypy.ini, prunes the corresponding 138 lines
from docs/typing/mypy_baseline.txt (5279 → 5141), and fixes the errors
that surface under the current mypy / django-stubs pins.

Per-file changes:
- test_annotation_admin.py: class-level annotations on TestAnnotationAdmin
  for every cls.<attr> in setUpTestData; get_user_model() → direct User
  import.
- test_annotation_images_api.py: class-level annotations on
  AnnotationImagesAPITestCase; same User import switch (also clears the
  valid-type error on the `owner: User` parameter).
- test_annotation_permission_mutations.py: no code change; the historical
  set_permissions_for_obj_to_user arg-type errors no longer reproduce.
- test_annotation_privacy.py: same as above.
- test_annotation_tree.py: rename self.client → self.graphene_client on
  AnnotationTreeTestCase so the graphene Client no longer shadows the
  inherited django.test.TestCase.client (mypy resolved .execute through
  Django's Client); replace one assertIsNotNone with `assert ... is not
  None` narrowing; same User import switch.
- test_annotation_utils.py: class-level annotations on the two TestCases;
  scoped # type: ignore[arg-type] on the two compute_content_modalities
  calls that deliberately pass mixed/invalid items to exercise defensive
  skipping; same User import switch.

Refs #1738

Signed-off-by: JSIV <5049984+JSv4@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
pull Bot pushed a commit to osamakaram/OpenContracts that referenced this pull request Jun 5, 2026
Continues the mypy baseline drain (issue Open-Source-Legal#1738, the Open-Source-Legal#1331 -> Open-Source-Legal#1335 -> Open-Source-Legal#1447
cadence). After the config tail (Open-Source-Legal#1748), tests.permissioning.* (Open-Source-Legal#1768), and
the annotation (Open-Source-Legal#1776) / document (Open-Source-Legal#1851) / corpus (Open-Source-Legal#1866) / agent (Open-Source-Legal#1877)
leaf-file chunks, this is the next cohesive batch from the issue's tests.*
batching plan: the extract domain.

Removes the 6 [mypy-opencontractserver.tests.test_*] ignore_errors blocks for
the structured-data-extraction feature -- Extracts plus their Fieldsets/Columns,
Datacells, and metadata columns (all in opencontractserver/extracts/models.py):
test_extract_mutations, test_extract_queries, test_extract_tasks,
test_datacell_mutations, test_column_mutations, test_metadata_columns_graphql.
Prunes the corresponding 57 lines from docs/typing/mypy_baseline.txt
(3648 -> 3591) and fixes the 42 errors that surface.

Every surfaced error was the same: a graphene.test.Client assigned to
self.client shadows django.test.TestCase.client (which has no .execute()).
Renamed self.client -> self.graphene_client in the 5 GraphQL test modules -- a
behaviour-equivalent rename, since the assignment already shadowed the Django
client at runtime and every read was .execute(). test_extract_tasks needed no
code change: its historical set_permissions_for_obj_to_user arg-type errors no
longer reproduce under django-stubs==6.0.5.

Full project surface (mypy --config-file mypy.ini opencontractserver config)
stays clean under both the pre-commit pin (mypy==2.0.0) and CI's pin
(mypy==2.1.0).

Refs Open-Source-Legal#1738
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants