From 3b44baa7f02ef587e32b3afdf00ecb4ebae2a1cf Mon Sep 17 00:00:00 2001 From: Adam Dangoor Date: Mon, 7 Sep 2026 22:22:54 +0100 Subject: [PATCH 1/3] Remove workarounds for closed pyrefly issue 1897 pyrefly 1.2.0 supports copy.replace, so the explicit annotations and ignore comments are no longer needed. Co-Authored-By: Claude Fable 5.1 --- src/mock_vws/_flask_server/target_manager.py | 15 ++++++--------- .../mock_web_services_api.py | 10 ++++------ src/mock_vws/decorators.py | 7 ++----- 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/src/mock_vws/_flask_server/target_manager.py b/src/mock_vws/_flask_server/target_manager.py index aa99f054e..3471b4bcd 100644 --- a/src/mock_vws/_flask_server/target_manager.py +++ b/src/mock_vws/_flask_server/target_manager.py @@ -787,9 +787,8 @@ def delete_target(database_name: str, target_id: str) -> Response: target = database.get_target(target_id=target_id) now = datetime.datetime.now(tz=target.upload_date.tzinfo) - # See https://github.com/facebook/pyrefly/issues/1897 - new_target: ImageTarget = copy.replace( - target, # pyrefly: ignore[bad-argument-type] + new_target = copy.replace( + target, delete_date=now, ) database.targets.remove(target) @@ -831,9 +830,8 @@ def update_target(database_name: str, target_id: str) -> Response: image_value = target.image_value if "image" in request_json: image_value = base64.b64decode(s=request_json["image"]) - # See https://github.com/facebook/pyrefly/issues/1897 - new_target: ImageTarget = copy.replace( - target, # pyrefly: ignore[bad-argument-type] + new_target = copy.replace( + target, name=name, width=width, active_flag=active_flag, @@ -891,9 +889,8 @@ def set_target_recognition_counts( target = database.get_target(target_id=target_id) - # See https://github.com/facebook/pyrefly/issues/1897 - new_target: ImageTarget = copy.replace( - target, # pyrefly: ignore[bad-argument-type] + new_target = copy.replace( + target, current_month_recos=request_json.get( "current_month_recos", target.current_month_recos, diff --git a/src/mock_vws/_requests_mock_server/mock_web_services_api.py b/src/mock_vws/_requests_mock_server/mock_web_services_api.py index 068f6bf70..c1787573a 100644 --- a/src/mock_vws/_requests_mock_server/mock_web_services_api.py +++ b/src/mock_vws/_requests_mock_server/mock_web_services_api.py @@ -624,9 +624,8 @@ def delete_target(self, request: RequestData) -> _ResponseType: ) now = datetime.datetime.now(tz=target.upload_date.tzinfo) - # See https://github.com/facebook/pyrefly/issues/1897 - new_target: ImageTarget = copy.replace( - target, # pyrefly: ignore[bad-argument-type] + new_target = copy.replace( + target, delete_date=now, ) database.targets.remove(target) @@ -1039,9 +1038,8 @@ def update_target(self, request: RequestData) -> _ResponseType: fail_exception.response_text, ) - # See https://github.com/facebook/pyrefly/issues/1897 - new_target: ImageTarget = copy.replace( - target, # pyrefly: ignore[bad-argument-type] + new_target = copy.replace( + target, name=name, width=width, active_flag=active_flag, diff --git a/src/mock_vws/decorators.py b/src/mock_vws/decorators.py index aaa5793c8..3b5982dd7 100644 --- a/src/mock_vws/decorators.py +++ b/src/mock_vws/decorators.py @@ -38,7 +38,6 @@ ModelTargetGenerationFailure, ModelTargetGenerationWarning, ) -from mock_vws.target import ImageTarget from mock_vws.target_manager import TargetManager from mock_vws.target_raters import ( BrisqueTargetTrackingRater, @@ -377,10 +376,8 @@ def set_target_recognition_counts( for cloud_database, target in matches: # Recognizing a target does not change it, so the target's # last modified date is not changed here. - # - # See https://github.com/facebook/pyrefly/issues/1897 - new_target: ImageTarget = copy.replace( - target, # pyrefly: ignore[bad-argument-type] + new_target = copy.replace( + target, current_month_recos=( target.current_month_recos if current_month_recos is None From 16787e6c209dae6d79679e174694fdb2832f4061 Mon Sep 17 00:00:00 2001 From: Adam Dangoor Date: Mon, 7 Sep 2026 22:36:01 +0100 Subject: [PATCH 2/3] Make pyrefly error on unused ignore comments Co-Authored-By: Claude Fable 5.1 --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index dcd4c3a2c..24beb8bdd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -454,6 +454,7 @@ search_path = [ "src", ] errors.non-exhaustive-match = "error" +errors.unused-ignore = "error" [tool.pyright] typeCheckingMode = "strict" From 03a048431b2e1426dcc6533b268110bb734d607c Mon Sep 17 00:00:00 2001 From: Adam Dangoor Date: Mon, 7 Sep 2026 22:53:10 +0100 Subject: [PATCH 3/3] Use the pyrefly strict preset The strict preset enables unused-ignore along with implicit-any, missing-override-decorator and other checks. Fix the new errors: add @override decorators, replace an untyped lambda sort key with a typed function, and type two empty containers. Co-Authored-By: Claude Fable 5.1 --- pyproject.toml | 2 +- .../_httpx2_mock_server/decorators.py | 4 +++- src/mock_vws/_mock_common.py | 22 ++++++++++++++----- src/mock_vws/_model_target_web_api.py | 2 +- tests/mock_vws/test_model_target_web_api.py | 2 +- 5 files changed, 23 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 24beb8bdd..293fd4822 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -454,7 +454,7 @@ search_path = [ "src", ] errors.non-exhaustive-match = "error" -errors.unused-ignore = "error" +preset = "strict" [tool.pyright] typeCheckingMode = "strict" diff --git a/src/mock_vws/_httpx2_mock_server/decorators.py b/src/mock_vws/_httpx2_mock_server/decorators.py index 919dec38d..764573261 100644 --- a/src/mock_vws/_httpx2_mock_server/decorators.py +++ b/src/mock_vws/_httpx2_mock_server/decorators.py @@ -10,7 +10,7 @@ import re from collections.abc import Callable, Mapping, Sequence from dataclasses import dataclass -from typing import Protocol +from typing import Protocol, override from unittest import mock from urllib.parse import urlparse @@ -173,6 +173,7 @@ def __init__( self._fakes = fakes self._wrapped = wrapped + @override def handle_request(self, request: httpx2.Request) -> httpx2.Response: """Handle a request from a synchronous ``httpx2`` client. @@ -217,6 +218,7 @@ def __init__( self._fakes = fakes self._wrapped = wrapped + @override async def handle_async_request( self, request: httpx2.Request, diff --git a/src/mock_vws/_mock_common.py b/src/mock_vws/_mock_common.py index 45def7600..3e14af108 100644 --- a/src/mock_vws/_mock_common.py +++ b/src/mock_vws/_mock_common.py @@ -1,11 +1,12 @@ """Common utilities for creating mock routes.""" +import datetime import email.utils import json import uuid from collections.abc import Iterable, Mapping from dataclasses import dataclass -from typing import Any +from typing import Any, override from beartype import beartype @@ -34,6 +35,7 @@ def __init__(self, url: str) -> None: super().__init__() self.url = url + @override def __str__(self) -> str: """ Give a string representation of this error with a @@ -80,6 +82,19 @@ class Route: http_methods: Iterable[str] +@beartype +def _upload_order_key(target: ImageTarget) -> tuple[datetime.datetime, str]: + """Give the sort key which orders targets by upload date, then ID. + + Args: + target: The target to give a sort key for. + + Returns: + The target's upload date and ID. + """ + return (target.upload_date, target.target_id) + + @beartype def sorted_targets(*, targets: Iterable[ImageTarget]) -> list[ImageTarget]: """Put targets into a deterministic order. @@ -94,10 +109,7 @@ def sorted_targets(*, targets: Iterable[ImageTarget]) -> list[ImageTarget]: Returns: The given targets, ordered by upload date and then by target ID. """ - return sorted( - targets, - key=lambda target: (target.upload_date, target.target_id), - ) + return sorted(targets, key=_upload_order_key) @beartype diff --git a/src/mock_vws/_model_target_web_api.py b/src/mock_vws/_model_target_web_api.py index c6084486c..761b589e9 100644 --- a/src/mock_vws/_model_target_web_api.py +++ b/src/mock_vws/_model_target_web_api.py @@ -1335,7 +1335,7 @@ def _validate_dataset_request( for model in models: if isinstance(model.get("views"), list): model["views"] = [ - view if isinstance(view, dict) else {} + view if isinstance(view, dict) else dict[str, Any]() for view in model["views"] ] details = ( diff --git a/tests/mock_vws/test_model_target_web_api.py b/tests/mock_vws/test_model_target_web_api.py index 600c989cd..a47c785e2 100644 --- a/tests/mock_vws/test_model_target_web_api.py +++ b/tests/mock_vws/test_model_target_web_api.py @@ -2394,7 +2394,7 @@ def test_client_credential_authentication_errors() -> None: """Credential-management routes enforce bearer-token validity and scope. """ - headers = [ + headers: list[dict[str, str]] = [ {}, {"Authorization": "Bearer malformed"}, {"Authorization": "Bearer e30.e30.signature"},