From f651bc1014faf4df00b4a2ff8ee6b288962bc3ee Mon Sep 17 00:00:00 2001 From: James Brunton Date: Thu, 14 May 2026 11:27:39 +0100 Subject: [PATCH] Add Pyrefly config and fix type errors --- .taskfiles/engine.yml | 2 +- engine/pyproject.toml | 38 ++++++++++++------- engine/src/stirling/agents/ledger/agent.py | 2 +- engine/src/stirling/agents/pdf_edit.py | 14 ++++++- engine/src/stirling/config/settings.py | 2 +- engine/src/stirling/services/tracking.py | 2 +- .../agents/test_orchestrator_pdf_comment.py | 2 +- engine/tests/agents/test_whole_doc_reader.py | 18 +++++++-- engine/tests/ledger/test_models.py | 7 ++-- engine/tests/ledger/test_routes.py | 3 +- engine/tests/test_documents.py | 24 ++++++++++-- engine/tests/test_documents_routes.py | 2 +- engine/tests/test_pdf_question_agent.py | 2 +- engine/uv.lock | 33 +++++++--------- 14 files changed, 96 insertions(+), 55 deletions(-) diff --git a/.taskfiles/engine.yml b/.taskfiles/engine.yml index 339ae1b297..7ecb845421 100644 --- a/.taskfiles/engine.yml +++ b/.taskfiles/engine.yml @@ -75,7 +75,7 @@ tasks: desc: "Run type checking" deps: [install] cmds: - - uv run pyright . --warnings + - uv run pyrefly check test: desc: "Run tests" diff --git a/engine/pyproject.toml b/engine/pyproject.toml index 4074c21ee2..b54b2d56b5 100644 --- a/engine/pyproject.toml +++ b/engine/pyproject.toml @@ -23,7 +23,7 @@ dev = [ "anyio>=4.0.0", "datamodel-code-generator[ruff]>=0.26.0", "pytest>=8.0.0", - "pyright>=1.1.408", + "pyrefly==1.0.0", "referencing>=0.35.0", "ruff>=0.14.10", ] @@ -51,21 +51,11 @@ select = [ "W", "RUF100", "UP", - "PYI", # flake8-pyi: flags deprecated typing constructs - "FA", # flake8-future-annotations: flags missing future annotations imports - "BLE", # flake8-blind-except: flags bare `except Exception` + "PYI", # flake8-pyi: flags deprecated typing constructs + "FA", # flake8-future-annotations: flags missing future annotations imports + "BLE", # flake8-blind-except: flags bare `except Exception` ] -[tool.pyright] -pythonVersion = "3.13" - -reportImportCycles = "warning" -reportUnnecessaryCast = "warning" -reportUnnecessaryTypeIgnoreComment = "warning" -reportUnusedImport = "warning" -reportUnknownParameterType = "warning" -reportDeprecated = "warning" - [tool.pytest.ini_options] testpaths = ["tests"] # ``tests`` is on the path so test modules can import shared helpers (e.g. @@ -74,3 +64,23 @@ pythonpath = ["src", "tests"] # Use importlib import mode so test directories don't need __init__.py files # and duplicate basenames (e.g. multiple test_routes.py) collect cleanly. addopts = "--import-mode=importlib" + +[tool.pyrefly] +python-version = "3.13.0" +search-path = [ + "src", + "tests", +] +# Disallow `type: ignore` comments to encourage accurate error codes to be used in ignore comments +# Pyrefly won't flag `type: ignore [wrong-code]`, but it will flag `pyrefly: ignore [wrong-code]` +enabled-ignores = ["pyrefly"] +min-severity = "warn" # TODO: Change settings below to `warn` - https://github.com/facebook/pyrefly/issues/3370 + +[tool.pyrefly.errors] +deprecated = "error" +implicit-any = "error" +redundant-cast = "error" +unused-ignore = "error" +# TODO: Add equivalents to: +# - reportImportCycles +# - reportUnusedImport diff --git a/engine/src/stirling/agents/ledger/agent.py b/engine/src/stirling/agents/ledger/agent.py index f000b604e4..c675bd28bb 100644 --- a/engine/src/stirling/agents/ledger/agent.py +++ b/engine/src/stirling/agents/ledger/agent.py @@ -478,6 +478,7 @@ class MathAuditorAgent: logger.info("[math-auditor-agent] extracting figures from page %d (%d chars)", folio.page, len(text)) prompt = f"Page {folio.page + 1} text:\n{text}" + figures: list[ExtractedFigure] = [] try: result = await self._figure_extractor.run(prompt) figures = result.output.figures @@ -487,7 +488,6 @@ class MathAuditorAgent: folio.page, exc_info=True, ) - figures = [] logger.debug( "TOOL (extract_figures)\nArgs: %s\nResult: %s", diff --git a/engine/src/stirling/agents/pdf_edit.py b/engine/src/stirling/agents/pdf_edit.py index 4dac512c1b..5ab137f76d 100644 --- a/engine/src/stirling/agents/pdf_edit.py +++ b/engine/src/stirling/agents/pdf_edit.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging from collections.abc import Iterable -from typing import Literal, overload +from typing import Literal, Protocol, overload from pydantic import Field from pydantic_ai import Agent @@ -72,6 +72,16 @@ class PdfEditSelectionAgent: return result.output +class ParameterSelector(Protocol): + async def select( + self, + request: PdfEditRequest, + operation_plan: list[ToolEndpoint], + operation_index: int, + generated_steps: list[ToolOperationStep], + ) -> ParamToolModel: ... + + class PdfEditParameterSelector: def __init__(self, runtime: AppRuntime) -> None: self.runtime = runtime @@ -141,7 +151,7 @@ class PdfEditParameterSelector: class PdfEditAgent: def __init__(self, runtime: AppRuntime) -> None: self.runtime = runtime - self.parameter_selector = PdfEditParameterSelector(runtime) + self.parameter_selector: ParameterSelector = PdfEditParameterSelector(runtime) async def orchestrate(self, request: OrchestratorRequest) -> PdfEditResponse: """Entry point for the orchestrator delegate — adapts the orchestrator's diff --git a/engine/src/stirling/config/settings.py b/engine/src/stirling/config/settings.py index 3b9fd24799..a18ce42525 100644 --- a/engine/src/stirling/config/settings.py +++ b/engine/src/stirling/config/settings.py @@ -131,7 +131,7 @@ def _enable_http_debug(formatter: logging.Formatter) -> None: lg.setLevel(level) # Idempotent: avoid stacking handlers on settings reload. if not any(getattr(h, "_stirling_http_debug", False) for h in lg.handlers): - handler._stirling_http_debug = True # type: ignore[attr-defined] + handler._stirling_http_debug = True # pyrefly: ignore [missing-attribute] lg.addHandler(handler) lg.propagate = False diff --git a/engine/src/stirling/services/tracking.py b/engine/src/stirling/services/tracking.py index f8c93524a1..e78e86d025 100644 --- a/engine/src/stirling/services/tracking.py +++ b/engine/src/stirling/services/tracking.py @@ -69,7 +69,7 @@ def _transform_output_choices(choices: list[Any]) -> list[Any]: for choice in choices: if not isinstance(choice, dict) or "parts" not in choice: continue - tool_calls = [] + tool_calls: list[dict[str, Any]] = [] for part in choice.get("parts", []): if isinstance(part, dict) and part.get("type") == "tool_call": tool_calls.append( diff --git a/engine/tests/agents/test_orchestrator_pdf_comment.py b/engine/tests/agents/test_orchestrator_pdf_comment.py index a9aabaa562..2a7e5d230c 100644 --- a/engine/tests/agents/test_orchestrator_pdf_comment.py +++ b/engine/tests/agents/test_orchestrator_pdf_comment.py @@ -50,7 +50,7 @@ async def test_delegate_pdf_review_wires_prompt_to_tool_step(runtime: AppRuntime "stirling.agents.pdf_review.MathIntentClassifier.classify", new=AsyncMock(return_value=False), ): - response = await orchestrator.delegate_pdf_review(ctx) # type: ignore[arg-type] + response = await orchestrator.delegate_pdf_review(ctx) # pyrefly: ignore [bad-argument-type] assert isinstance(response, EditPlanResponse) assert len(response.steps) == 1 diff --git a/engine/tests/agents/test_whole_doc_reader.py b/engine/tests/agents/test_whole_doc_reader.py index 588a43f370..a44f797dc2 100644 --- a/engine/tests/agents/test_whole_doc_reader.py +++ b/engine/tests/agents/test_whole_doc_reader.py @@ -54,7 +54,7 @@ class StubEmbedder: @pytest.fixture def runtime_with_stub_docs(runtime: AppRuntime) -> AppRuntime: stub = DocumentService( - embedder=StubEmbedder(), # type: ignore[arg-type] + embedder=StubEmbedder(), # pyrefly: ignore [bad-argument-type] store=SqliteVecStore.ephemeral(), default_top_k=runtime.settings.rag_default_top_k, ) @@ -198,13 +198,25 @@ async def test_read_full_document_budget_hides_tool_when_exhausted( sentinel: object = object() # Budget intact -> prepare returns the tool. - assert await capability._prepare_read_full_document(None, sentinel) is sentinel # type: ignore[arg-type] + assert ( + await capability._prepare_read_full_document( + None, # pyrefly: ignore [bad-argument-type] + sentinel, # pyrefly: ignore [bad-argument-type] + ) + is sentinel + ) # Spend the budget. await capability._read_full_document("anything") # Budget spent -> prepare returns None. - assert await capability._prepare_read_full_document(None, sentinel) is None # type: ignore[arg-type] + assert ( + await capability._prepare_read_full_document( + None, # pyrefly: ignore [bad-argument-type] + sentinel, # pyrefly: ignore [bad-argument-type] + ) + is None + ) @pytest.mark.anyio diff --git a/engine/tests/ledger/test_models.py b/engine/tests/ledger/test_models.py index 98b0ef9c85..68a0f55ce0 100644 --- a/engine/tests/ledger/test_models.py +++ b/engine/tests/ledger/test_models.py @@ -35,11 +35,10 @@ def test_folio_manifest_round_trip() -> None: assert reloaded == manifest -def test_folio_manifest_round_bounds() -> None: +@pytest.mark.parametrize("round_value", [0, 4]) +def test_folio_manifest_round_bounds(round_value: int) -> None: with pytest.raises(ValidationError): - FolioManifest(session_id="x", page_count=1, folio_types=[FolioType.TEXT], round=0) - with pytest.raises(ValidationError): - FolioManifest(session_id="x", page_count=1, folio_types=[FolioType.TEXT], round=4) + FolioManifest(session_id="x", page_count=1, folio_types=[FolioType.TEXT], round=round_value) # --------------------------------------------------------------------------- diff --git a/engine/tests/ledger/test_routes.py b/engine/tests/ledger/test_routes.py index ca322a6364..535bf1819b 100644 --- a/engine/tests/ledger/test_routes.py +++ b/engine/tests/ledger/test_routes.py @@ -12,6 +12,7 @@ from collections.abc import Iterator from decimal import Decimal import pytest +from conftest import build_app_settings from fastapi.testclient import TestClient from stirling.api import app @@ -34,8 +35,6 @@ from stirling.contracts.ledger import ( class StubSettingsProvider: def __call__(self) -> AppSettings: - from conftest import build_app_settings - return build_app_settings() diff --git a/engine/tests/test_documents.py b/engine/tests/test_documents.py index a5febf078f..8ec429e057 100644 --- a/engine/tests/test_documents.py +++ b/engine/tests/test_documents.py @@ -152,7 +152,11 @@ class StubEmbeddingService: def documents() -> DocumentService: """Each DocumentService test gets its own fresh ephemeral store to avoid dimension conflicts.""" store = SqliteVecStore.ephemeral() - return DocumentService(embedder=StubEmbeddingService(), store=store, default_top_k=3) # type: ignore[arg-type] + return DocumentService( + embedder=StubEmbeddingService(), # pyrefly: ignore [bad-argument-type] + store=store, + default_top_k=3, + ) def _pages(text: str) -> list[PageText]: @@ -265,7 +269,7 @@ async def _invoke_search_knowledge(capability: RagCapability, query: str, max_re toolset = capability.toolset assert isinstance(toolset, FunctionToolset) tool = toolset.tools["search_knowledge"] - return await tool.function(query=query, max_results=max_results) # type: ignore[call-arg] + return await tool.function(query=query, max_results=max_results) class TestRagCapability: @@ -345,12 +349,24 @@ class TestRagCapability: cap = RagCapability(documents, max_searches=2) tool_def = _dummy_tool_def() - assert await cap._prepare_search_knowledge(None, tool_def) is tool_def # type: ignore[arg-type] + assert ( + await cap._prepare_search_knowledge( + None, # pyrefly: ignore [bad-argument-type] + tool_def, # pyrefly: ignore [bad-argument-type] + ) + is tool_def + ) await _invoke_search_knowledge(cap, "content") await _invoke_search_knowledge(cap, "content") - assert await cap._prepare_search_knowledge(None, tool_def) is None # type: ignore[arg-type] + assert ( + await cap._prepare_search_knowledge( + None, # pyrefly: ignore [bad-argument-type] + tool_def, # pyrefly: ignore [bad-argument-type] + ) + is None + ) def _dummy_tool_def() -> object: diff --git a/engine/tests/test_documents_routes.py b/engine/tests/test_documents_routes.py index 844d785185..7362275849 100644 --- a/engine/tests/test_documents_routes.py +++ b/engine/tests/test_documents_routes.py @@ -45,7 +45,7 @@ class StubEmbedder: def _build_service() -> DocumentService: return DocumentService( - embedder=StubEmbedder(), # type: ignore[arg-type] + embedder=StubEmbedder(), # pyrefly: ignore [bad-argument-type] store=SqliteVecStore.ephemeral(), default_top_k=3, ) diff --git a/engine/tests/test_pdf_question_agent.py b/engine/tests/test_pdf_question_agent.py index 26bd5c0fff..a4bf5cdeef 100644 --- a/engine/tests/test_pdf_question_agent.py +++ b/engine/tests/test_pdf_question_agent.py @@ -68,7 +68,7 @@ class StubPdfQuestionAgent(PdfQuestionAgent): def runtime_with_stub_rag(runtime: AppRuntime) -> AppRuntime: """A runtime whose document service uses a stub embedder + ephemeral store.""" stub = DocumentService( - embedder=StubEmbedder(), # type: ignore[arg-type] + embedder=StubEmbedder(), # pyrefly: ignore [bad-argument-type] store=SqliteVecStore.ephemeral(), default_top_k=runtime.settings.rag_default_top_k, ) diff --git a/engine/uv.lock b/engine/uv.lock index c8f79e6372..237322933a 100644 --- a/engine/uv.lock +++ b/engine/uv.lock @@ -621,7 +621,7 @@ dependencies = [ dev = [ { name = "anyio" }, { name = "datamodel-code-generator", extra = ["ruff"] }, - { name = "pyright" }, + { name = "pyrefly" }, { name = "pytest" }, { name = "referencing" }, { name = "ruff" }, @@ -647,7 +647,7 @@ requires-dist = [ dev = [ { name = "anyio", specifier = ">=4.0.0" }, { name = "datamodel-code-generator", extras = ["ruff"], specifier = ">=0.26.0" }, - { name = "pyright", specifier = ">=1.1.408" }, + { name = "pyrefly", specifier = "==1.0.0" }, { name = "pytest", specifier = ">=8.0.0" }, { name = "referencing", specifier = ">=0.35.0" }, { name = "ruff", specifier = ">=0.14.10" }, @@ -1671,15 +1671,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/13/04/eaac430d0e6bf21265ae989427d37e94be5e41dc216879f1fbb6c5339942/nexus_rpc-1.2.0-py3-none-any.whl", hash = "sha256:977876f3af811ad1a09b2961d3d1ac9233bda43ff0febbb0c9906483b9d9f8a3", size = 28166, upload-time = "2025-11-17T19:17:05.64Z" }, ] -[[package]] -name = "nodeenv" -version = "1.10.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, -] - [[package]] name = "numpy" version = "2.4.4" @@ -2500,16 +2491,20 @@ wheels = [ ] [[package]] -name = "pyright" -version = "1.1.408" +name = "pyrefly" +version = "1.0.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "nodeenv" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/74/b2/5db700e52554b8f025faa9c3c624c59f1f6c8841ba81ab97641b54322f16/pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684", size = 4400578, upload-time = "2026-01-08T08:07:38.795Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/3a/9045b0097ac58979c7c30a4fa0e673db942d4adbc7b6d439bd54ae58c441/pyrefly-1.0.0.tar.gz", hash = "sha256:5c2b810ffcebd84be71de5df1223651edee951653a66935c6f091e957c452455", size = 5677995, upload-time = "2026-05-12T20:12:46.812Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/82/a2c93e32800940d9573fb28c346772a14778b84ba7524e691b324620ab89/pyright-1.1.408-py3-none-any.whl", hash = "sha256:090b32865f4fdb1e0e6cd82bf5618480d48eecd2eb2e70f960982a3d9a4c17c1", size = 6399144, upload-time = "2026-01-08T08:07:37.082Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c6/90788819bac9c61dd7bacba53b79f3c12d47ccbe5e51b3d6d89f2387e1d2/pyrefly-1.0.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e355a0908555348ed4b9585ef25c76ff566673e345c866c325f1633f44d890b6", size = 13122950, upload-time = "2026-05-12T20:12:20.711Z" }, + { url = "https://files.pythonhosted.org/packages/82/91/a3cf2a1e87d336eaa804a1e6fc93266faf6dc2a97eecdbc7eae289628022/pyrefly-1.0.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a7038efc3a40f8294edee339895633cf22db268c0d434cdbcbefc34f78a9ecc3", size = 12599494, upload-time = "2026-05-12T20:12:23.495Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ab/74d1e11e737e99b1c003ecc5d7d2e846c4ea1f328966bfdbbd0ac63fad0a/pyrefly-1.0.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da331ca515ed1c08791da2b5f664cf9c1294c48fd802133262e7d5d51e0f4416", size = 12995507, upload-time = "2026-05-12T20:12:25.951Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ac/2df0899f8464c97e5d995f994c97c5cb5b0f58610432aa90d26d924e1db5/pyrefly-1.0.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c74219d8f3e63cdaa5501a0b21d1c9d37011820f9606728d0ed06f09ae86a878", size = 13947693, upload-time = "2026-05-12T20:12:29.188Z" }, + { url = "https://files.pythonhosted.org/packages/6b/3e/b247c24321e36f04b7d51f9ccf3df93e5009e4b29939524b36ec2e17dc2a/pyrefly-1.0.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c0d05543b1bb6ee6d64149eb5d6b2fb15aa72d3962d6a97abca0afaca8b0c131", size = 13925803, upload-time = "2026-05-12T20:12:31.904Z" }, + { url = "https://files.pythonhosted.org/packages/61/16/cfa2d61a4aa1e1f7bca48bb37acd01c6a09db4864b16a54f9587092765ff/pyrefly-1.0.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1382d5b1fcdb49a4de9f34d112d2bddf290a78ff93ee8149492ad5f1077ddffc", size = 13470398, upload-time = "2026-05-12T20:12:35.302Z" }, + { url = "https://files.pythonhosted.org/packages/cb/2b/6372c7dddb326223e24a46b17efd0d4bd7b4fe22c821e523157577eed2d2/pyrefly-1.0.0-py3-none-win32.whl", hash = "sha256:aa8b5d0e47080e3202a2547b39f7a5a61d2c781c712b3b67884f745ca2c759d2", size = 12222643, upload-time = "2026-05-12T20:12:38.618Z" }, + { url = "https://files.pythonhosted.org/packages/be/ad/1d23be700b6b2ddaeb362360c7145917a8edbbf7240ae428d40541772fce/pyrefly-1.0.0-py3-none-win_amd64.whl", hash = "sha256:c8abcb0f2082e83c890375128f9cff4aa4d3f210b85eea7b3046c1ae764e77f5", size = 13146369, upload-time = "2026-05-12T20:12:41.423Z" }, + { url = "https://files.pythonhosted.org/packages/8c/38/16589134f3012fd097a10dcc85771555f1a5fb76e04b682597180743af30/pyrefly-1.0.0-py3-none-win_arm64.whl", hash = "sha256:d150fa9e40e8392832be81c3bcfc0497c146674ce4d0f8e04e1ec29e775ffb8c", size = 12538326, upload-time = "2026-05-12T20:12:43.996Z" }, ] [[package]]