[META]: Migrate pyright to ty#86
Open
spencrr wants to merge 13 commits into
Open
Conversation
Aligns RAMPART with sister project PyRIT (#1319), which moved off mypy
to ty for type checking. RAMPART previously used pyright (strict). ty
is Astral's Rust-based type checker; the migration adopts the same
shape PyRIT settled on but tightened for a smaller codebase.
Changes:
- pyproject.toml: drop pyright dev dep, add ty>=0.0.38; replace
[tool.pyright] config with [tool.ty.environment], [tool.ty.src], and
[tool.ty.rules]. Add [[tool.ty.overrides]] for tests/** that disables
six rule categories where the tests intentionally feed MagicMock
objects to typed pytest hooks, construct objects with deliberately
wrong args to exercise validation paths, and define Protocol stub
classes with '...' bodies.
- .pre-commit-config.yaml: replace pyright hook with ty hook.
- .github/workflows/ci.yml: replace 'uv run pyright' step with
'uv run ty check'.
- Source fixes in rampart/:
- attacks/__init__.py: annotate 'handles: list[InjectionHandle]'
so the union-of-list branches collapse cleanly; ty cannot yet
narrow element type through isinstance(x, list), so the one
remaining branch carries a targeted suppression.
- pytest_plugin/plugin.py: replace the lambda passed to
register_default_handler_factory with a typed module-level
function so the Protocol return type aligns (list invariance).
Replace pyright suppressions for dynamic pytest.Item attributes
with ty equivalents.
- attacks/_xpia.py, drivers/_utils.py, evaluators/response_contains.py:
add narrow # ty: ignore comments with explanations for ty's
current inference gaps (Self-typed __aenter__, list[A]|list[B]
isinstance narrowing, callable() narrowing against str-containing
unions).
- Strip all '# pyright: ignore[...]' comments project-wide; tests/**
overrides cover the equivalent test-only cases.
- Update contributor docs (development-setup, code-style,
architecture, pull-requests, contributing index) to reference ty.
Verification:
- uv run ty check: All checks passed
- uv run ruff check . / ruff format --check .: clean
- uv run pytest: 448 passed
- uv run pre-commit run --all-files: all hooks pass
ty rejected `AsyncExitStack.enter_async_context(handle)` because `InjectionHandle` was structurally close to but not assignable to `AbstractAsyncContextManager[InjectionHandle, None]` — `__aenter__` returning `Self` doesn't unify with the parent's covariant `_T_co` when there's no explicit subtype relationship. Inheriting from `AbstractAsyncContextManager["InjectionHandle", None]` fixes this. Notes: - typeshed declares `AbstractAsyncContextManager` as both `ABC` and `Protocol` (it's on the type-checker allowlist), so a Protocol can legally inherit from it. See: https://github.com/python/typeshed/blob/main/stdlib/contextlib.pyi - Base order matters: the ABC must come before `Protocol` for C3 linearization to succeed. - Generic args `[InjectionHandle, None]`: `_T_co` = what `__aenter__` returns, `_ExitT_co` = `None` (we never suppress exceptions). - `__aenter__ -> Self` is kept as a covariant refinement of the parent's `_T_co`; subclasses get their precise self-type. - `__aexit__` aligned with stdlib conventions: positional-only `/` marker and canonical `exc_value`/`traceback` parameter names.
nina-msft
reviewed
Jun 18, 2026
Use exc_value/traceback parameter names and add positional-only / marker, matching the InjectionHandle.__aexit__ signature (typeshed/contextlib.pyi). Cosmetic — ty accepts both forms; the asymmetry was just confusing for readers. Addresses PR microsoft#86 review.
InjectionHandle is not in rampart.attacks.__all__ — it was only importable from there as an incidental runtime side-effect of the new isinstance check in Attacks.xpia. Import from rampart.core.injection directly so the test doesn't depend on a re-export that could quietly disappear. Addresses PR microsoft#86 review.
The `if found is True` check was a behavioral regression: callable targets can return any truthy value (e.g., `re.Match` from `lambda t: re.search(pat, t)`), and `Match is True` is False. That caused silent NOT_DETECTED where the previous `if found:` correctly returned DETECTED — a false negative in a safety detector. ty doesn't require `is True` either: after the isinstance reorder, `found` is statically `bool`, so plain `if found:` type-checks fine. The isinstance reordering above is kept. Addresses PR microsoft#86 review.
Two stragglers from the pyright->ty migration: the 'tooling' bullet (line 35) describing strict-mode Pyright, and the pre-commit summary (line 160) naming Pyright as the type checker. Both now reference ty. Not addressing the .gitignore portion of the review comment: leaving `pyrightconfig.json` ignored is intentional (default from the Python gitignore template). Addresses PR microsoft#86 review.
Remove the [[tool.ty.overrides]] block that silenced invalid-argument-type across all of tests/**. That was too broad — it hid genuine wrong-arg-type bugs anywhere in the suite, not just the intentional negative tests. Two narrow categories of fix replace it: - MagicMock passed to a concrete (non-Protocol) param: cast at the call site to the expected type (pytest.Config / TerminalReporter / pytest.Session). The variable stays a MagicMock so .assert_*() and attribute mutation still work. MagicMock already satisfies Protocol params structurally (via __getattr__ -> Any), so only concrete-class params needed this. - Intentional wrong types asserting a runtime error (None config fields, 'not a function', NotASink) inside pytest.raises: per-line # ty: ignore[invalid-argument-type]. One non-mock case (register_default_handler_factory(lambda: [handler])) failed on list invariance, not a mock — fixed with a typed local list[ExecutionEventHandler] instead of a cast or ignore. Verification: uv run ty check clean; ruff clean; 491 passed, 5 skipped.
…ocol stubs `@abstractmethod` on a plain (non-ABC) class is a runtime no-op, and these classes are instantiated immediately for `isinstance(..., Protocol)` checks. The decorator read as the opposite of intent — it was only there to quiet the type checker about the `...` body not returning `Session`. `raise NotImplementedError` is clearer, works with any return type, and drops the now -unused `abc.abstractmethod` import. Applied in both the conforming-adapter stub (test_adapter.py) and the structural-subtyping stub (test_protocols.py). Addresses PR microsoft#86 review (comment 3432228031).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Migrate type checker from pyright to ty to align with PyRIT (microsoft/PyRIT#1319).
Typing fixes prompted by ty
InjectionHandleandSessionnow inheritAbstractAsyncContextManager(rampart/core/injection.py, rampart/core/adapter.py). ty rejectedAsyncExitStack.enter_async_context(handle)because aProtocolwith__aenter__ -> Selfdoesn't unify withAbstractAsyncContextManager[Handle, _ExitT_co]'s covariant_T_cowithout an explicit subtype relationship.Notes:
AbstractAsyncContextManageras bothABCandProtocol(stdlib/contextlib.pyi) - it's on the type-checker allowlist, so aProtocolmay legally inherit from it.Protocol.[InjectionHandle, None]:_T_co=__aenter__return;_ExitT_co=Nonebecause cleanup never suppresses exceptions (closing/nullcontextfamily, notsuppress).__aexit__aligned with stdlib conventions: positional-only/, canonicalexc_value/tracebacknames.Attacks.xpiaandcoerce_drivernarrowing reordered (rampart/attacks/init.py, rampart/drivers/_utils.py). ty cannot yet narrow element type throughisinstance(x, list)overlist[A] | list[B]. Reordering to check the protocol/scalar branch first eliminates the need forcastand# type: ignore.ResponseContainsbranch order tightened (rampart/evaluators/response_contains.py).callable()doesn't narrow against a union containingstr(every str is callable for ty's purposes here). Reordering tore.Pattern->str->callableand usingif found is True:makes the dispatch unambiguous.pytest_plugin/plugin.py(rampart/pytest_plugin/plugin.py): replaced aregister_default_handler_factorylambda with a typed module-level_default_handler_factorysolist[ExecutionEventHandler]aligns under list invariance.pytest.Itemuser-attribute writes now use# ty: ignore[unresolved-attribute].Test changes
_mock_handleand_mock_evaluatornow usespec=InjectionHandle/spec=Evaluator. With the reordered narrowing inAttacks.xpia(isinstance(inject, InjectionHandle)first), bareAsyncMock()becomes ambiguous because runtime-checkable protocols treat any Mock as matching (everyhasattrreturns True).spec=makesisinstancedeterministic.@abstractmethodadded to a protocol-conformance test fixture so ty doesn't flag the...body.# pyright: ignore-># ty: ignore(e.g., tests/unit/core/test_llm.py).Docs
Five contributing pages updated to reference ty: docs/contributing/index.md, development-setup.md, code-style.md, architecture.md, pull-requests.md.
Breaking changes
None for users. Contributors will need to run
uv syncto install ty (pyright is removed from the dev group) and update editor integrations - call out in development-setup.md.Checklist
pre-commit run --all-filespassestests/unit/attacks/test_xpia.pymocks gainspec=for deterministic protocolisinstance; pyright ignore strings ported intests/unit/core/test_llm.py, test_execution.py,test_protocols.py, test_types.py, etc.Verification
uv run ty check: All checks passeduv run ruff check . && uv run ruff format --check .: cleanuv run pytest: 491 passed, 5 skippeduv run pre-commit run --all-files: all hooks pass