From cc5f1964bcb2a31554d9b8cb638849fdfe8d7c2f Mon Sep 17 00:00:00 2001 From: Adam Dangoor Date: Tue, 8 Sep 2026 10:16:49 +0100 Subject: [PATCH 1/2] Use the pyrefly all preset --- pyproject.toml | 5 ++- spelling_private_dict.txt | 1 + src/vws_cli/_error_handling.py | 11 +++++-- src/vws_cli/commands.py | 7 ++-- src/vws_cli/model_target.py | 26 ++++++++------- src/vws_cli/options/_types.py | 17 ++++++++++ src/vws_cli/options/credentials.py | 25 +++++++-------- src/vws_cli/options/model_targets.py | 25 +++++++-------- src/vws_cli/options/targets.py | 18 +++++------ src/vws_cli/options/timeout.py | 13 ++++---- src/vws_cli/options/vws.py | 13 ++++---- src/vws_cli/vumark.py | 2 +- tests/test_error_handling.py | 6 ++-- tests/test_model_target.py | 33 ++++++++++--------- tests/test_query.py | 44 +++++++++++++------------ tests/test_query_errors.py | 24 +++++++------- tests/test_reco_counts_report.py | 12 +++---- tests/test_vws_commands.py | 48 ++++++++++++++-------------- tests/test_vws_errors.py | 40 +++++++++++------------ uv.lock | 14 ++++++++ 20 files changed, 213 insertions(+), 171 deletions(-) create mode 100644 src/vws_cli/options/_types.py diff --git a/pyproject.toml b/pyproject.toml index 5ea9af70..a174bae1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -87,6 +87,7 @@ optional-dependencies.dev = [ "towncrier==26.9.0", "ty==0.0.78", "types-pyyaml==6.0.12.20260815", + "types-requests==2.33.0.20260712", "vale==3.20.0.0", "vulture==2.16", "vws-python-mock==2026.8.26.1", @@ -357,6 +358,8 @@ exclude = [ # Ideally we would limit the paths to the source code where we want to ignore names, # but Vulture does not enable this. ignore_names = [ + # Structural Protocol member used through Click decorator objects. + "__call__", # Sphinx "autoclass_content", "autoclass_content", @@ -410,7 +413,7 @@ plugins = [ [tool.pyrefly] errors.non-exhaustive-match = "error" -preset = "strict" +preset = "all" [tool.pyright] typeCheckingMode = "strict" diff --git a/spelling_private_dict.txt b/spelling_private_dict.txt index c45f684b..60fd2fcb 100644 --- a/spelling_private_dict.txt +++ b/spelling_private_dict.txt @@ -28,6 +28,7 @@ png pragma pre pyperclip +pyrefly pyright pytest reco diff --git a/src/vws_cli/_error_handling.py b/src/vws_cli/_error_handling.py index 8c86bcbd..40adca3b 100644 --- a/src/vws_cli/_error_handling.py +++ b/src/vws_cli/_error_handling.py @@ -122,12 +122,19 @@ def get_model_target_error_message( case ModelTargetValidationError(): problems = [ f"{detail.code}: {detail.message}" for detail in exc.details - ] or [exc.message] + ] + if len(problems) == 0: + problems = [exc.message] message = "\n".join( ["Error: Vuforia rejected the request.", *problems], ) case ModelTargetError(): - message = f"Error: {exc.message or 'Vuforia returned an error.'}" + error_message = ( + exc.message + if exc.message != "" + else "Vuforia returned an error." + ) + message = f"Error: {error_message}" case _: message = get_error_message(exc=exc) diff --git a/src/vws_cli/commands.py b/src/vws_cli/commands.py index b705daf9..77ea0017 100644 --- a/src/vws_cli/commands.py +++ b/src/vws_cli/commands.py @@ -252,8 +252,9 @@ def get_target_summary_report( ) report = vws_client.get_target_summary_report(target_id=target_id) report_dict = dataclasses.asdict(obj=report) - report_dict["status"] = report_dict["status"].value - report_dict["upload_date"] = str(object=report_dict["upload_date"]) + report_dict["status"] = report.status.value + upload_date: object = report_dict["upload_date"] + report_dict["upload_date"] = str(object=upload_date) yaml_summary_report = yaml.dump(data=report_dict) click.echo(message=yaml_summary_report) @@ -650,4 +651,4 @@ def get_database_reco_counts_report( click.echo(message=report.raw_csv, nl=False) return - output_file_path.write_bytes(data=report.raw_csv) + _ = output_file_path.write_bytes(data=report.raw_csv) diff --git a/src/vws_cli/model_target.py b/src/vws_cli/model_target.py index 3f56e2ef..beddfce3 100644 --- a/src/vws_cli/model_target.py +++ b/src/vws_cli/model_target.py @@ -171,19 +171,21 @@ def _is_json_array(*, value: object) -> bool: @beartype -def _as_json_object(*, value: object) -> dict[str, Any] | None: +def _as_json_object(*, value: object) -> dict[str, Any] | None: # pyrefly: ignore[explicit-any] """Get an object from a models file, or ``None``.""" if not _is_json_object(value=value): return None # The value goes through a variable which is typed as ``Any`` so that # the keys and values of the returned object are not unknown types. - value_any: Any = value - value_dict: dict[str, Any] = value_any + value_any: Any = value # pyrefly: ignore[explicit-any] + value_dict: dict[str, Any] = ( # pyrefly: ignore[explicit-any] + value_any + ) return value_dict @beartype -def _json_object(*, value: object, message: str) -> dict[str, Any]: +def _json_object(*, value: object, message: str) -> dict[str, Any]: # pyrefly: ignore[explicit-any] """Get an object from a models file, or raise an error.""" value_dict = _as_json_object(value=value) if value_dict is None: @@ -192,14 +194,14 @@ def _json_object(*, value: object, message: str) -> dict[str, Any]: @beartype -def _json_array(*, value: object, message: str) -> list[Any]: +def _json_array(*, value: object, message: str) -> list[Any]: # pyrefly: ignore[explicit-any] """Get an array from a models file, or raise an error.""" if not _is_json_array(value=value): raise _models_file_error(message=message) # The value goes through a variable which is typed as ``Any`` so that # the items of the returned array are not unknown types. - value_any: Any = value - value_list: list[Any] = value_any + value_any: Any = value # pyrefly: ignore[explicit-any] + value_list: list[Any] = value_any # pyrefly: ignore[explicit-any] return value_list @@ -210,14 +212,14 @@ def _checked_object( known_fields: frozenset[str], required_fields: Sequence[str], path: str, -) -> dict[str, Any]: +) -> dict[str, Any]: # pyrefly: ignore[explicit-any] """Get an object with known and required fields, or raise an error.""" value_dict = _json_object( value=value, message=f"{path} must be an object.", ) unknown_fields = sorted(set(value_dict) - known_fields) - if unknown_fields: + if bool(unknown_fields): message = f"{path} has unknown fields: {', '.join(unknown_fields)}." raise _models_file_error(message=message) @@ -333,7 +335,7 @@ def _model_from_json(*, value: object, path: str) -> ModelTargetModel: path=path, ) - model_kwargs: dict[str, Any] = { + model_kwargs: dict[str, Any] = { # pyrefly: ignore[explicit-any] field_name: _string_value( value=model_dict[json_field], path=f"{path}/{json_field}", @@ -566,7 +568,7 @@ def create_model_target_dataset( for option_name, value in model_option_values.items() if value is not None ) - if given_model_options: + if bool(given_model_options): message = ( "--models-file cannot be used with " f"{', '.join(given_model_options)}." @@ -801,7 +803,7 @@ def download_model_target_dataset( dataset_type=dataset_type, ) - output_file_path.write_bytes(data=dataset) + _ = output_file_path.write_bytes(data=dataset) @click.command(name="delete-model-target-dataset") diff --git a/src/vws_cli/options/_types.py b/src/vws_cli/options/_types.py new file mode 100644 index 00000000..6ed39dc3 --- /dev/null +++ b/src/vws_cli/options/_types.py @@ -0,0 +1,17 @@ +"""Shared typing helpers for Click option decorators.""" + +from collections.abc import Callable +from typing import Protocol, runtime_checkable + + +@runtime_checkable +class ClickOptionDecorator(Protocol): + """A decorator which preserves a command's signature.""" + + def __call__[**P, R]( + self, + command: Callable[P, R], + /, + ) -> Callable[P, R]: + """Decorate ``command`` without changing its signature.""" + raise NotImplementedError diff --git a/src/vws_cli/options/credentials.py b/src/vws_cli/options/credentials.py index 353c7c8f..1424750d 100644 --- a/src/vws_cli/options/credentials.py +++ b/src/vws_cli/options/credentials.py @@ -1,16 +1,15 @@ """``click`` options regarding credentials.""" from collections.abc import Callable -from typing import Any import click from beartype import beartype @beartype -def server_access_key_option( - command: Callable[..., Any], -) -> Callable[..., Any]: +def server_access_key_option[**P, R]( + command: Callable[P, R], +) -> Callable[P, R]: """An option decorator for the Vuforia server access key.""" return click.option( "--server-access-key", @@ -26,9 +25,9 @@ def server_access_key_option( @beartype -def server_secret_key_option( - command: Callable[..., Any], -) -> Callable[..., Any]: +def server_secret_key_option[**P, R]( + command: Callable[P, R], +) -> Callable[P, R]: """An option decorator for the Vuforia server secret key.""" return click.option( "--server-secret-key", @@ -44,9 +43,9 @@ def server_secret_key_option( @beartype -def client_access_key_option( - command: Callable[..., Any], -) -> Callable[..., Any]: +def client_access_key_option[**P, R]( + command: Callable[P, R], +) -> Callable[P, R]: """An option decorator for the Vuforia client access key.""" return click.option( "--client-access-key", @@ -62,9 +61,9 @@ def client_access_key_option( @beartype -def client_secret_key_option( - command: Callable[..., Any], -) -> Callable[..., Any]: +def client_secret_key_option[**P, R]( + command: Callable[P, R], +) -> Callable[P, R]: """An option decorator for the Vuforia client secret key.""" return click.option( "--client-secret-key", diff --git a/src/vws_cli/options/model_targets.py b/src/vws_cli/options/model_targets.py index b04dc2ef..c9ec1353 100644 --- a/src/vws_cli/options/model_targets.py +++ b/src/vws_cli/options/model_targets.py @@ -1,7 +1,6 @@ """``click`` options regarding Model Target datasets.""" from collections.abc import Callable -from typing import Any import click from beartype import beartype @@ -9,9 +8,9 @@ @beartype -def client_id_option( - command: Callable[..., Any], -) -> Callable[..., Any]: +def client_id_option[**P, R]( + command: Callable[P, R], +) -> Callable[P, R]: """An option decorator for the Model Target Web API client ID.""" return click.option( "--client-id", @@ -27,9 +26,9 @@ def client_id_option( @beartype -def client_secret_option( - command: Callable[..., Any], -) -> Callable[..., Any]: +def client_secret_option[**P, R]( + command: Callable[P, R], +) -> Callable[P, R]: """An option decorator for the Model Target Web API client secret.""" return click.option( "--client-secret", @@ -45,9 +44,9 @@ def client_secret_option( @beartype -def dataset_type_option( - command: Callable[..., Any], -) -> Callable[..., Any]: +def dataset_type_option[**P, R]( + command: Callable[P, R], +) -> Callable[P, R]: """An option decorator for the kind of Model Target dataset.""" return click.option( "--dataset-type", @@ -66,9 +65,9 @@ def dataset_type_option( @beartype -def dataset_uuid_option( - command: Callable[..., Any], -) -> Callable[..., Any]: +def dataset_uuid_option[**P, R]( + command: Callable[P, R], +) -> Callable[P, R]: """An option decorator for the UUID of a Model Target dataset.""" return click.option( "--dataset-uuid", diff --git a/src/vws_cli/options/targets.py b/src/vws_cli/options/targets.py index 884dee67..39fdcf67 100644 --- a/src/vws_cli/options/targets.py +++ b/src/vws_cli/options/targets.py @@ -1,14 +1,14 @@ """``click`` options regarding targets.""" -from collections.abc import Callable from enum import Enum, unique from pathlib import Path -from typing import Any import click from beartype import beartype -target_id_option: Callable[..., Any] = click.option( +from vws_cli.options._types import ClickOptionDecorator + +target_id_option: ClickOptionDecorator = click.option( "--target-id", type=str, help="The ID of a target in the Vuforia database.", @@ -17,7 +17,7 @@ @beartype -def target_name_option(*, required: bool) -> Callable[..., Any]: +def target_name_option(*, required: bool) -> ClickOptionDecorator: """An option decorator for choosing a target name.""" return click.option( "--name", @@ -28,9 +28,9 @@ def target_name_option(*, required: bool) -> Callable[..., Any]: @beartype -def target_width_option(*, required: bool) -> Callable[..., Any]: +def target_width_option(*, required: bool) -> ClickOptionDecorator: """An option decorator for choosing a target width.""" - option: Callable[..., Any] = click.option( + option: ClickOptionDecorator = click.option( "--width", type=float, help="The width of the target in the Vuforia database.", @@ -40,7 +40,7 @@ def target_width_option(*, required: bool) -> Callable[..., Any]: @beartype -def target_image_option(*, required: bool) -> Callable[..., Any]: +def target_image_option(*, required: bool) -> ClickOptionDecorator: """An option decorator for choosing a target image.""" return click.option( "--image", @@ -68,7 +68,7 @@ class ActiveFlagChoice(Enum): def active_flag_option( *, allow_none: bool, -) -> Callable[..., Any]: +) -> ClickOptionDecorator: """An option decorator for setting a target's active flag.""" if allow_none: default = None @@ -87,7 +87,7 @@ def active_flag_option( ) -application_metadata_option: Callable[..., Any] = click.option( +application_metadata_option: ClickOptionDecorator = click.option( "--application-metadata", type=str, required=False, diff --git a/src/vws_cli/options/timeout.py b/src/vws_cli/options/timeout.py index bf8bfad3..da0c1f49 100644 --- a/src/vws_cli/options/timeout.py +++ b/src/vws_cli/options/timeout.py @@ -1,16 +1,15 @@ """``click`` options regarding timeouts.""" from collections.abc import Callable -from typing import Any import click from beartype import beartype @beartype -def connection_timeout_seconds_option( - command: Callable[..., Any], -) -> Callable[..., Any]: +def connection_timeout_seconds_option[**P, R]( + command: Callable[P, R], +) -> Callable[P, R]: """An option decorator for the connection timeout.""" return click.option( "--connection-timeout-seconds", @@ -22,9 +21,9 @@ def connection_timeout_seconds_option( @beartype -def read_timeout_seconds_option( - command: Callable[..., Any], -) -> Callable[..., Any]: +def read_timeout_seconds_option[**P, R]( + command: Callable[P, R], +) -> Callable[P, R]: """An option decorator for the read timeout.""" return click.option( "--read-timeout-seconds", diff --git a/src/vws_cli/options/vws.py b/src/vws_cli/options/vws.py index c944d58d..fcabc932 100644 --- a/src/vws_cli/options/vws.py +++ b/src/vws_cli/options/vws.py @@ -1,16 +1,15 @@ """``click`` options for VWS API options.""" from collections.abc import Callable -from typing import Any import click from beartype import beartype @beartype -def database_id_option( - command: Callable[..., Any], -) -> Callable[..., Any]: +def database_id_option[**P, R]( + command: Callable[P, R], +) -> Callable[P, R]: """An option decorator for the Vuforia database ID.""" return click.option( "--database-id", @@ -26,9 +25,9 @@ def database_id_option( @beartype -def base_vws_url_option( - command: Callable[..., Any], -) -> Callable[..., Any]: +def base_vws_url_option[**P, R]( + command: Callable[P, R], +) -> Callable[P, R]: """An option decorator for choosing the base VWS URL.""" return click.option( "--base-vws-url", diff --git a/src/vws_cli/vumark.py b/src/vws_cli/vumark.py index 1f6634bd..4b4cf8db 100644 --- a/src/vws_cli/vumark.py +++ b/src/vws_cli/vumark.py @@ -154,4 +154,4 @@ def generate_vumark( accept=accept, ) - output_file_path.write_bytes(data=vumark_data) + _ = output_file_path.write_bytes(data=vumark_data) diff --git a/tests/test_error_handling.py b/tests/test_error_handling.py index f0db2590..e2167c85 100644 --- a/tests/test_error_handling.py +++ b/tests/test_error_handling.py @@ -59,7 +59,7 @@ def test_vumark_service_error( assert result.exit_code == 1 assert result.stderr == f"{expected_message}\n" - assert not result.stdout + assert not bool(result.stdout) def test_invalid_target_type( @@ -100,7 +100,7 @@ def test_invalid_target_type( assert result.exit_code == 1 assert result.stderr == "Error: The target type is invalid.\n" - assert not result.stdout + assert not bool(result.stdout) def test_too_many_requests() -> None: @@ -125,4 +125,4 @@ def test_too_many_requests() -> None: result.stderr == "Error: Too many requests were made to Vuforia. Try again later.\n" ) - assert not result.stdout + assert not bool(result.stdout) diff --git a/tests/test_model_target.py b/tests/test_model_target.py index b0b59b9b..88515cea 100644 --- a/tests/test_model_target.py +++ b/tests/test_model_target.py @@ -6,7 +6,6 @@ from collections.abc import Iterator from http import HTTPStatus from pathlib import Path -from typing import Any import pytest from click.testing import CliRunner @@ -106,7 +105,7 @@ def test_dataset_lifecycle(*, dataset_type: str, tmp_path: Path) -> None: color=True, ) assert wait_result.exit_code == 0 - assert not wait_result.stderr + assert not bool(wait_result.stderr) assert "status: done" in wait_result.stdout output_file_path = tmp_path / "dataset.zip" @@ -122,7 +121,7 @@ def test_dataset_lifecycle(*, dataset_type: str, tmp_path: Path) -> None: color=True, ) assert download_result.exit_code == 0 - assert not download_result.stdout + assert not bool(download_result.stdout) assert zipfile.is_zipfile(filename=output_file_path) delete_result = runner.invoke( @@ -132,7 +131,7 @@ def test_dataset_lifecycle(*, dataset_type: str, tmp_path: Path) -> None: color=True, ) assert delete_result.exit_code == 0 - assert not delete_result.stdout + assert not bool(delete_result.stdout) deleted_status_result = runner.invoke( cli=vws_group, @@ -174,7 +173,7 @@ def test_cad_data_file(*, tmp_path: Path) -> None: """A model's CAD data can be given as a file.""" runner = CliRunner() cad_data_file_path = tmp_path / "model.obj" - cad_data_file_path.write_bytes(data=b"\x00cad-data") + _ = cad_data_file_path.write_bytes(data=b"\x00cad-data") result = runner.invoke( cli=vws_group, args=[ @@ -193,7 +192,7 @@ def test_cad_data_file(*, tmp_path: Path) -> None: color=True, ) assert result.exit_code == 0 - assert result.stdout.strip() + assert bool(result.stdout.strip()) @pytest.mark.usefixtures("model_target_mock") @@ -201,7 +200,7 @@ def test_model_options(*, tmp_path: Path) -> None: """The optional model settings are sent to Vuforia.""" runner = CliRunner() state_based_configuration_file_path = tmp_path / "states.json" - state_based_configuration_file_path.write_text( + _ = state_based_configuration_file_path.write_text( data=json.dumps(obj={"states": {"open": {}}}), ) dataset_uuid = _create_dataset( @@ -223,13 +222,13 @@ def test_model_options(*, tmp_path: Path) -> None: str(object=state_based_configuration_file_path), ], ) - assert dataset_uuid + assert bool(dataset_uuid) def _models_file(*, tmp_path: Path, models_json: object) -> Path: """Write a models file, and return its path.""" models_file_path = tmp_path / "models.json" - models_file_path.write_text(data=json.dumps(obj=models_json)) + _ = models_file_path.write_text(data=json.dumps(obj=models_json)) return models_file_path @@ -286,7 +285,7 @@ def test_models_file(*, tmp_path: Path) -> None: color=True, ) assert result.exit_code == 0, result.output - assert result.stdout.strip() + assert bool(result.stdout.strip()) @pytest.mark.usefixtures("model_target_mock") @@ -315,7 +314,7 @@ def test_models_file_with_models_key(*, tmp_path: Path) -> None: color=True, ) assert result.exit_code == 0 - assert result.stdout.strip() + assert bool(result.stdout.strip()) @pytest.mark.usefixtures("model_target_mock") @@ -415,19 +414,19 @@ def test_one_cad_data_source_required(*, cad_data_args: list[str]) -> None: ) -_VALID_POSITION: dict[str, Any] = { +_VALID_POSITION: dict[str, object] = { "rotation": [0, 0, 0, 1], "translation": [0, 0, -1], } -_VALID_VIEW: dict[str, Any] = { +_VALID_VIEW: dict[str, object] = { "name": "front", "guideViewPosition": _VALID_POSITION, } -_EMPTY_OBJECT: dict[str, Any] = {} +_EMPTY_OBJECT: dict[str, object] = {} -_EMPTY_ARRAY: list[Any] = [] +_EMPTY_ARRAY: list[object] = [] @pytest.mark.parametrize( @@ -660,7 +659,7 @@ def test_models_file_is_not_json(*, tmp_path: Path) -> None: """An error is shown for a models file which is not JSON.""" runner = CliRunner() models_file_path = tmp_path / "models.json" - models_file_path.write_text(data="not-json") + _ = models_file_path.write_text(data="not-json") result = runner.invoke( cli=vws_group, args=[ @@ -945,7 +944,7 @@ def test_wait_for_dataset_with_warning() -> None: ) assert result.exit_code == 0 - assert not result.stderr + assert not bool(result.stderr) assert "status: done" in result.stdout assert f"message: {warning.message}" in result.stdout diff --git a/tests/test_query.py b/tests/test_query.py index 5fe1ea1d..f1b67a6a 100644 --- a/tests/test_query.py +++ b/tests/test_query.py @@ -32,7 +32,7 @@ def test_no_matches( runner = CliRunner() new_file = tmp_path / uuid.uuid4().hex image_data = high_quality_image.getvalue() - new_file.write_bytes(data=image_data) + _ = new_file.write_bytes(data=image_data) commands = [ str(object=new_file), "--client-access-key", @@ -72,7 +72,7 @@ def test_matches( runner = CliRunner() new_file = tmp_path / uuid.uuid4().hex image_data = high_quality_image.getvalue() - new_file.write_bytes(data=image_data) + _ = new_file.write_bytes(data=image_data) commands = [ str(object=new_file), "--client-access-key", @@ -89,7 +89,9 @@ def test_matches( assert result.exit_code == 0 result_data = yaml.safe_load(stream=result.stdout) [matching_target] = result_data - target_timestamp = matching_target["target_data"]["target_timestamp"] + target_timestamp: object = matching_target["target_data"][ + "target_timestamp" + ] expected_result_data = { "target_data": { "application_metadata": None, @@ -127,7 +129,7 @@ def test_image_file_is_dir( ) expected_result_code = 2 assert result.exit_code == expected_result_code - assert not result.stdout + assert not bool(result.stdout) expected_stderr = dedent( text=f"""\ Usage: vuforia-cloud-reco [OPTIONS] IMAGE @@ -150,7 +152,7 @@ def test_relative_path( new_filename = uuid.uuid4().hex original_image_file = tmp_path / "foo" image_data = high_quality_image.getvalue() - original_image_file.write_bytes(data=image_data) + _ = original_image_file.write_bytes(data=image_data) commands = [ str(object=new_filename), "--client-access-key", @@ -199,7 +201,7 @@ def test_image_file_does_not_exist( ) expected_result_code = 2 assert result.exit_code == expected_result_code - assert not result.stdout + assert not bool(result.stdout) expected_stderr = dedent( text=f"""\ Usage: vuforia-cloud-reco [OPTIONS] IMAGE @@ -232,7 +234,7 @@ def test_default_timeout( runner = CliRunner() new_file = tmp_path / uuid.uuid4().hex image_data = high_quality_image.getvalue() - new_file.write_bytes(data=image_data) + _ = new_file.write_bytes(data=image_data) with ( freeze_time() as frozen_datetime, MockVWS( @@ -259,7 +261,7 @@ def test_default_timeout( with pytest.raises( expected_exception=requests.exceptions.Timeout, ): - runner.invoke( + _ = runner.invoke( cli=vuforia_cloud_reco, args=commands, catch_exceptions=False, @@ -288,7 +290,7 @@ def test_custom_timeout( runner = CliRunner() new_file = tmp_path / uuid.uuid4().hex image_data = high_quality_image.getvalue() - new_file.write_bytes(data=image_data) + _ = new_file.write_bytes(data=image_data) with ( freeze_time() as frozen_datetime, MockVWS( @@ -316,7 +318,7 @@ def test_custom_timeout( with pytest.raises( expected_exception=requests.exceptions.Timeout, ): - runner.invoke( + _ = runner.invoke( cli=vuforia_cloud_reco, args=commands, catch_exceptions=False, @@ -333,7 +335,7 @@ def test_custom_timeout_no_error( runner = CliRunner() new_file = tmp_path / uuid.uuid4().hex image_data = high_quality_image.getvalue() - new_file.write_bytes(data=image_data) + _ = new_file.write_bytes(data=image_data) with ( freeze_time() as frozen_datetime, MockVWS( @@ -413,7 +415,7 @@ def test_default( new_file = tmp_path / uuid.uuid4().hex image_data = high_quality_image.getvalue() - new_file.write_bytes(data=image_data) + _ = new_file.write_bytes(data=image_data) commands = [ str(object=new_file), "--client-access-key", @@ -468,7 +470,7 @@ def test_custom( new_file = tmp_path / uuid.uuid4().hex image_data = high_quality_image.getvalue() - new_file.write_bytes(data=image_data) + _ = new_file.write_bytes(data=image_data) max_num_results = 2 commands = [ str(object=new_file), @@ -500,7 +502,7 @@ def test_out_of_range( runner = CliRunner() new_file = tmp_path / uuid.uuid4().hex image_data = high_quality_image.getvalue() - new_file.write_bytes(data=image_data) + _ = new_file.write_bytes(data=image_data) commands = [ str(object=new_file), "--max-num-results", @@ -556,7 +558,7 @@ def test_default( vws_client.wait_for_target_processed(target_id=target_id_2) new_file = tmp_path / uuid.uuid4().hex image_data = high_quality_image.getvalue() - new_file.write_bytes(data=image_data) + _ = new_file.write_bytes(data=image_data) commands = [ str(object=new_file), "--max-num-results", @@ -609,7 +611,7 @@ def test_top( vws_client.wait_for_target_processed(target_id=target_id_2) new_file = tmp_path / uuid.uuid4().hex image_data = high_quality_image.getvalue() - new_file.write_bytes(data=image_data) + _ = new_file.write_bytes(data=image_data) commands = [ str(object=new_file), "--max-num-results", @@ -664,7 +666,7 @@ def test_none( vws_client.wait_for_target_processed(target_id=target_id_2) new_file = tmp_path / uuid.uuid4().hex image_data = high_quality_image.getvalue() - new_file.write_bytes(data=image_data) + _ = new_file.write_bytes(data=image_data) commands = [ str(object=new_file), "--max-num-results", @@ -716,7 +718,7 @@ def test_all( vws_client.wait_for_target_processed(target_id=target_id_2) new_file = tmp_path / uuid.uuid4().hex image_data = high_quality_image.getvalue() - new_file.write_bytes(data=image_data) + _ = new_file.write_bytes(data=image_data) commands = [ str(object=new_file), @@ -756,7 +758,7 @@ def test_other( runner = CliRunner() new_file = tmp_path / uuid.uuid4().hex image_data = high_quality_image.getvalue() - new_file.write_bytes(data=image_data) + _ = new_file.write_bytes(data=image_data) commands = [ str(object=new_file), "--max-num-results", @@ -794,7 +796,7 @@ def test_base_vwq_url( base_vwq_url = "http://example.com" new_file = tmp_path / uuid.uuid4().hex image_data = high_quality_image.getvalue() - new_file.write_bytes(data=image_data) + _ = new_file.write_bytes(data=image_data) with MockVWS(base_vwq_url=base_vwq_url) as mock: mock_database = CloudDatabase() mock.add_cloud_database(cloud_database=mock_database) @@ -847,7 +849,7 @@ def test_env_var_credentials( runner = CliRunner() new_file = tmp_path / uuid.uuid4().hex image_data = high_quality_image.getvalue() - new_file.write_bytes(data=image_data) + _ = new_file.write_bytes(data=image_data) commands = [str(object=new_file)] result = runner.invoke( cli=vuforia_cloud_reco, diff --git a/tests/test_query_errors.py b/tests/test_query_errors.py index ef3efad3..5361274e 100644 --- a/tests/test_query_errors.py +++ b/tests/test_query_errors.py @@ -38,7 +38,7 @@ def test_fallback_error( ) -> None: """Other Cloud Reco errors have a user-facing message.""" image_path = tmp_path / "image.jpg" - image_path.write_bytes(data=high_quality_image.getvalue()) + _ = image_path.write_bytes(data=high_quality_image.getvalue()) failure_response = CloudQueryFailureResponse( status_code=status_code, headers={"Content-Type": "text/plain"}, @@ -61,7 +61,7 @@ def test_fallback_error( assert result.exit_code == 1 assert result.stderr == f"{expected_message}\n" - assert not result.stdout + assert not bool(result.stdout) def test_authentication_failure( @@ -74,7 +74,7 @@ def test_authentication_failure( runner = CliRunner() new_file = tmp_path / uuid.uuid4().hex image_data = high_quality_image.getvalue() - new_file.write_bytes(data=image_data) + _ = new_file.write_bytes(data=image_data) commands = [ str(object=new_file), "--client-access-key", @@ -90,7 +90,7 @@ def test_authentication_failure( ) expected_stderr = "The given secret key was incorrect.\n" assert result.stderr == expected_stderr - assert not result.stdout + assert not bool(result.stdout) def test_image_too_large( @@ -103,7 +103,7 @@ def test_image_too_large( runner = CliRunner() new_file = tmp_path / uuid.uuid4().hex image_data = png_too_large.getvalue() - new_file.write_bytes(data=image_data) + _ = new_file.write_bytes(data=image_data) commands = [ str(object=new_file), "--client-access-key", @@ -119,7 +119,7 @@ def test_image_too_large( ) expected_stderr = "Error: The given image is too large.\n" assert result.stderr == expected_stderr - assert not result.stdout + assert not bool(result.stdout) def test_bad_image( @@ -132,7 +132,7 @@ def test_bad_image( For example, when a corrupt image is uploaded. """ new_file = tmp_path / uuid.uuid4().hex - new_file.write_bytes(data=b"Not an image") + _ = new_file.write_bytes(data=b"Not an image") runner = CliRunner() commands = [ str(object=new_file), @@ -152,7 +152,7 @@ def test_bad_image( "Error: The given image is corrupted or the format is not supported.\n" ) assert result.stderr == expected_stderr - assert not result.stdout + assert not bool(result.stdout) def test_inactive_project( @@ -167,7 +167,7 @@ def test_inactive_project( """ new_file = tmp_path / uuid.uuid4().hex image_data = high_quality_image.getvalue() - new_file.write_bytes(data=image_data) + _ = new_file.write_bytes(data=image_data) database = CloudDatabase(state=States.PROJECT_INACTIVE) with MockVWS() as mock: mock.add_cloud_database(cloud_database=database) @@ -191,7 +191,7 @@ def test_inactive_project( "Error: The project associated with the given keys is inactive.\n" ) assert result.stderr == expected_stderr - assert not result.stdout + assert not bool(result.stdout) def test_request_time_too_skewed( @@ -211,7 +211,7 @@ def test_request_time_too_skewed( time_difference_from_now = vwq_max_time_skew + leeway new_file = tmp_path / uuid.uuid4().hex image_data = high_quality_image.getvalue() - new_file.write_bytes(data=image_data) + _ = new_file.write_bytes(data=image_data) # We use a custom tick because we expect the following: # @@ -240,4 +240,4 @@ def test_request_time_too_skewed( "This may be because the system clock is out of sync.\n" ) assert result.stderr == expected_stderr - assert not result.stdout + assert not bool(result.stdout) diff --git a/tests/test_reco_counts_report.py b/tests/test_reco_counts_report.py index 2157536c..66c8197c 100644 --- a/tests/test_reco_counts_report.py +++ b/tests/test_reco_counts_report.py @@ -63,7 +63,7 @@ def test_get_database_reco_counts_report( color=True, ) assert result.exit_code == 0 - assert not result.stderr + assert not bool(result.stderr) assert result.stdout_bytes == _EXPECTED_CSV @@ -86,7 +86,7 @@ def test_report_is_not_available_immediately() -> None: assert no_wait_result.exit_code == 0 presigned_url = no_wait_result.stdout.strip() with pytest.raises(expected_exception=RecoCountsReportNotReadyError): - vws_client.download_reco_counts_report( + _ = vws_client.download_reco_counts_report( presigned_url=presigned_url, ) @@ -120,7 +120,7 @@ def test_output_file(*, tmp_path: Path) -> None: ) assert result.exit_code == 0 - assert not result.stdout + assert not bool(result.stdout) assert output_file_path.read_bytes() == _EXPECTED_CSV @@ -239,7 +239,7 @@ def test_month_out_of_range(*, mock_database: CloudDatabase) -> None: "processed. Check the given parameters.\n" ) assert result.stderr == expected_stderr - assert not result.stdout + assert not bool(result.stdout) def test_database_id_does_not_match( @@ -268,7 +268,7 @@ def test_database_id_does_not_match( assert result.exit_code == 1 expected_stderr = "The given secret key was incorrect.\n" assert result.stderr == expected_stderr - assert not result.stdout + assert not bool(result.stdout) def test_timeout_reached() -> None: @@ -296,4 +296,4 @@ def test_timeout_reached() -> None: "Error: The recognition counts report was not generated within the " "allowed limit.\n" ) - assert not result.stdout + assert not bool(result.stdout) diff --git a/tests/test_vws_commands.py b/tests/test_vws_commands.py index 345b4ce0..b60ddfdf 100644 --- a/tests/test_vws_commands.py +++ b/tests/test_vws_commands.py @@ -31,7 +31,7 @@ def test_get_database_summary_report( """It is possible to get a database summary report.""" runner = CliRunner() for name in ("a", "b"): - vws_client.add_target( + _ = vws_client.add_target( name=name, width=1, image=high_quality_image, @@ -238,7 +238,7 @@ def test_delete_target( color=True, ) assert result.exit_code == 0 - assert not result.stdout + assert not bool(result.stdout) assert vws_client.list_targets() == [] @@ -310,7 +310,7 @@ def test_default_timeout( runner = CliRunner() new_file = tmp_path / uuid.uuid4().hex image_data = high_quality_image.getvalue() - new_file.write_bytes(data=image_data) + _ = new_file.write_bytes(data=image_data) with ( freeze_time() as frozen_datetime, MockVWS( @@ -343,7 +343,7 @@ def test_default_timeout( with pytest.raises( expected_exception=requests.exceptions.Timeout, ): - runner.invoke( + _ = runner.invoke( cli=vws_group, args=commands, catch_exceptions=False, @@ -372,7 +372,7 @@ def test_custom_timeout( runner = CliRunner() new_file = tmp_path / uuid.uuid4().hex image_data = high_quality_image.getvalue() - new_file.write_bytes(data=image_data) + _ = new_file.write_bytes(data=image_data) with ( freeze_time() as frozen_datetime, MockVWS( @@ -406,7 +406,7 @@ def test_custom_timeout( with pytest.raises( expected_exception=requests.exceptions.Timeout, ): - runner.invoke( + _ = runner.invoke( cli=vws_group, args=commands, catch_exceptions=False, @@ -423,7 +423,7 @@ def test_custom_timeout_no_error( runner = CliRunner() new_file = tmp_path / uuid.uuid4().hex image_data = high_quality_image.getvalue() - new_file.write_bytes(data=image_data) + _ = new_file.write_bytes(data=image_data) with ( freeze_time() as frozen_datetime, MockVWS( @@ -480,7 +480,7 @@ def test_add_target( new_file = tmp_path / uuid.uuid4().hex name = uuid.uuid4().hex image_data = high_quality_image.getvalue() - new_file.write_bytes(data=image_data) + _ = new_file.write_bytes(data=image_data) width = secrets.choice(seq=range(1, 5000)) / 100 commands = [ "add-target", @@ -550,7 +550,7 @@ def test_image_file_does_not_exist( ) expected_result_code = 2 assert result.exit_code == expected_result_code - assert not result.stdout + assert not bool(result.stdout) expected_stderr = dedent( text=f"""\ Usage: vws add-target [OPTIONS] @@ -594,7 +594,7 @@ def test_image_file_is_dir( ) expected_result_code = 2 assert result.exit_code == expected_result_code - assert not result.stdout + assert not bool(result.stdout) expected_stderr = dedent( text=f"""\ Usage: vws add-target [OPTIONS] @@ -618,7 +618,7 @@ def test_relative_path( new_filename = uuid.uuid4().hex original_image_file = tmp_path / "foo" image_data = high_quality_image.getvalue() - original_image_file.write_bytes(data=image_data) + _ = original_image_file.write_bytes(data=image_data) name = uuid.uuid4().hex commands = [ "add-target", @@ -661,7 +661,7 @@ def test_custom_metadata( new_file = tmp_path / uuid.uuid4().hex name = uuid.uuid4().hex image_data = high_quality_image.getvalue() - new_file.write_bytes(data=image_data) + _ = new_file.write_bytes(data=image_data) application_metadata = uuid.uuid4().hex metadata_bytes = application_metadata.encode(encoding="ascii") base64_encoded_metadata_bytes = base64.b64encode(s=metadata_bytes) @@ -719,7 +719,7 @@ def test_custom_active_flag( runner = CliRunner() new_file = tmp_path / uuid.uuid4().hex image_data = high_quality_image.getvalue() - new_file.write_bytes(data=image_data) + _ = new_file.write_bytes(data=image_data) commands = [ "add-target", "--name", @@ -789,7 +789,7 @@ def test_wait_for_target_processed( color=True, ) assert result.exit_code == 0 - assert not result.stdout + assert not bool(result.stdout) report = vws_client.get_target_summary_report(target_id=target_id) assert report.status != TargetStatuses.PROCESSING @@ -831,7 +831,7 @@ def test_default_seconds_between_requests( color=True, ) assert result.exit_code == 0 - assert not result.stdout + assert not bool(result.stdout) report = vws_client.get_database_summary_report() expected_requests = ( # Add target request @@ -903,7 +903,7 @@ def test_custom_seconds_between_requests( color=True, ) assert result.exit_code == 0 - assert not result.stdout + assert not bool(result.stdout) report = vws_client.get_database_summary_report() expected_requests = ( # Add target request @@ -955,7 +955,7 @@ def test_custom_seconds_too_small(mock_database: CloudDatabase) -> None: color=True, ) assert result.exit_code != 0 - assert not result.stdout + assert not bool(result.stdout) expected_substring = "0.01 is not in the range x>=0.05." assert expected_substring in result.stderr @@ -1087,7 +1087,7 @@ def test_update_target( new_width = secrets.choice(seq=range(1, 5000)) / 100 new_image_file = tmp_path / uuid.uuid4().hex new_image_data = different_high_quality_image.getvalue() - new_image_file.write_bytes(data=new_image_data) + _ = new_image_file.write_bytes(data=new_image_data) commands = [ "update-target", @@ -1115,7 +1115,7 @@ def test_update_target( color=True, ) assert result.exit_code == 0 - assert not result.stdout + assert not bool(result.stdout) vws_client.wait_for_target_processed(target_id=target_id) [ @@ -1145,7 +1145,7 @@ def test_update_target( color=True, ) assert result.exit_code == 0 - assert not result.stdout + assert not bool(result.stdout) target_details = vws_client.get_target_record(target_id=target_id) target_record = target_details.target_record assert not target_record.active_flag @@ -1189,7 +1189,7 @@ def test_no_fields_given( color=True, ) assert result.exit_code == 0 - assert not result.stdout + assert not bool(result.stdout) @staticmethod def test_image_file_does_not_exist( @@ -1232,7 +1232,7 @@ def test_image_file_does_not_exist( ) expected_result_code = 2 assert result.exit_code == expected_result_code - assert not result.stdout + assert not bool(result.stdout) expected_stderr = dedent( text=f"""\ Usage: vws update-target [OPTIONS] @@ -1284,7 +1284,7 @@ def test_image_file_is_dir( ) expected_result_code = 2 assert result.exit_code == expected_result_code - assert not result.stdout + assert not bool(result.stdout) expected_stderr = dedent( text=f"""\ Usage: vws update-target [OPTIONS] @@ -1316,7 +1316,7 @@ def test_relative_path( new_filename = uuid.uuid4().hex original_image_file = tmp_path / "foo" image_data = high_quality_image.getvalue() - original_image_file.write_bytes(data=image_data) + _ = original_image_file.write_bytes(data=image_data) commands = [ "update-target", "--target-id", diff --git a/tests/test_vws_errors.py b/tests/test_vws_errors.py index fdef975a..9cb3ebf9 100644 --- a/tests/test_vws_errors.py +++ b/tests/test_vws_errors.py @@ -39,7 +39,7 @@ def test_target_id_does_not_exist(mock_database: CloudDatabase) -> None: assert result.exit_code == 1 expected_stderr = 'Error: Target "abc12345" does not exist.\n' assert result.stderr == expected_stderr - assert not result.stdout + assert not bool(result.stdout) def test_bad_image( @@ -52,7 +52,7 @@ def test_bad_image( For example, when a corrupt image is uploaded. """ new_file = tmp_path / uuid.uuid4().hex - new_file.write_bytes(data=b"Not an image") + _ = new_file.write_bytes(data=b"Not an image") runner = CliRunner() args = [ "add-target", @@ -73,7 +73,7 @@ def test_bad_image( "Error: The given image is corrupted or the format is not supported.\n" ) assert result.stderr == expected_stderr - assert not result.stdout + assert not bool(result.stdout) def test_fail_bad_request( @@ -111,7 +111,7 @@ def test_fail_bad_request( "Check the given parameters.\n" ) assert result.stderr == expected_stderr - assert not result.stdout + assert not bool(result.stdout) def test_metadata_too_large( @@ -147,7 +147,7 @@ def test_metadata_too_large( assert result.exit_code == 1 expected_stderr = "Error: The given metadata is too large.\n" assert result.stderr == expected_stderr - assert not result.stdout + assert not bool(result.stdout) def test_image_too_large( @@ -160,7 +160,7 @@ def test_image_too_large( runner = CliRunner() new_file = tmp_path / uuid.uuid4().hex image_data = png_too_large.getvalue() - new_file.write_bytes(data=image_data) + _ = new_file.write_bytes(data=image_data) commands = [ "add-target", "--name", @@ -183,7 +183,7 @@ def test_image_too_large( assert result.exit_code == 1 expected_stderr = "Error: The given image is too large.\n" assert result.stderr == expected_stderr - assert not result.stdout + assert not bool(result.stdout) def test_target_name_exist( @@ -198,7 +198,7 @@ def test_target_name_exist( name. """ name = "foobar" - vws_client.add_target( + _ = vws_client.add_target( name=name, width=1, image=high_quality_image, @@ -209,7 +209,7 @@ def test_target_name_exist( runner = CliRunner() new_file = tmp_path / uuid.uuid4().hex image_data = high_quality_image.getvalue() - new_file.write_bytes(data=image_data) + _ = new_file.write_bytes(data=image_data) commands = [ "add-target", "--name", @@ -232,7 +232,7 @@ def test_target_name_exist( assert result.exit_code == 1 expected_stderr = 'Error: There is already a target named "foobar".\n' assert result.stderr == expected_stderr - assert not result.stdout + assert not bool(result.stdout) def test_project_inactive( @@ -247,7 +247,7 @@ def test_project_inactive( """ new_file = tmp_path / uuid.uuid4().hex image_data = high_quality_image.getvalue() - new_file.write_bytes(data=image_data) + _ = new_file.write_bytes(data=image_data) database = CloudDatabase(state=States.PROJECT_INACTIVE) with MockVWS() as mock: mock.add_cloud_database(cloud_database=database) @@ -277,7 +277,7 @@ def test_project_inactive( "Error: The project associated with the given keys is inactive.\n" ) assert result.stderr == expected_stderr - assert not result.stdout + assert not bool(result.stdout) def test_project_has_no_api_access( @@ -291,7 +291,7 @@ def test_project_has_no_api_access( """ new_file = tmp_path / uuid.uuid4().hex image_data = high_quality_image.getvalue() - new_file.write_bytes(data=image_data) + _ = new_file.write_bytes(data=image_data) database = CloudDatabase(state=States.PROJECT_HAS_NO_API_ACCESS) with MockVWS() as mock: mock.add_cloud_database(cloud_database=database) @@ -322,7 +322,7 @@ def test_project_has_no_api_access( "not allowed to make API requests.\n" ) assert result.stderr == expected_stderr - assert not result.stdout + assert not bool(result.stdout) def test_unknown_vws_error( @@ -339,7 +339,7 @@ def test_unknown_vws_error( runner = CliRunner() new_file = tmp_path / uuid.uuid4().hex image_data = high_quality_image.getvalue() - new_file.write_bytes(data=image_data) + _ = new_file.write_bytes(data=image_data) max_char_value = 65535 bad_name = chr(max_char_value + 1) @@ -368,7 +368,7 @@ def test_unknown_vws_error( "This may be because there is a problem with the given name.\n" ) assert result.stderr == expected_stderr - assert not result.stdout + assert not bool(result.stdout) def test_target_status_processing( @@ -413,7 +413,7 @@ def test_target_status_processing( "processing state.\n" ) assert result.stderr == expected_stderr - assert not result.stdout + assert not bool(result.stdout) def test_target_status_not_success( @@ -458,7 +458,7 @@ def test_target_status_not_success( "the success state.\n" ) assert result.stderr == expected_stderr - assert not result.stdout + assert not bool(result.stdout) def test_authentication_failure(mock_database: CloudDatabase) -> None: @@ -481,7 +481,7 @@ def test_authentication_failure(mock_database: CloudDatabase) -> None: assert result.exit_code == 1 expected_stderr = "The given secret key was incorrect.\n" assert result.stderr == expected_stderr - assert not result.stdout + assert not bool(result.stdout) def test_request_time_too_skewed(mock_database: CloudDatabase) -> None: @@ -522,4 +522,4 @@ def test_request_time_too_skewed(mock_database: CloudDatabase) -> None: "This may be because the system clock is out of sync.\n" ) assert result.stderr == expected_stderr - assert not result.stdout + assert not bool(result.stdout) diff --git a/uv.lock b/uv.lock index 062a23ff..2674801a 100644 --- a/uv.lock +++ b/uv.lock @@ -2184,6 +2184,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/08/52/eefeba09be4ef2a1eb989eb92934561e8e502a6ee3c32654996e4be7e399/types_pyyaml-6.0.12.20260815-py3-none-any.whl", hash = "sha256:6f332212b7e191f3afd5016a713c510b6340593b7ebec573c7d5d20aa5386d3b", size = 21148, upload-time = "2026-08-15T02:41:50.555Z" }, ] +[[package]] +name = "types-requests" +version = "2.33.0.20260712" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/51/703318f7b7be8bee126ec13bf615050f932d0179b8784420f3a0199cc769/types_requests-2.33.0.20260712.tar.gz", hash = "sha256:2141b67ab534a5c5cd2dac5034f2a35f42e699c5bf185eee608c5246a069d7fb", size = 25084, upload-time = "2026-07-12T05:14:20.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/e7/010c87f559e216d83f9dc51e939633fd0d0ead3377340181ab0e223cd3b5/types_requests-2.33.0.20260712-py3-none-any.whl", hash = "sha256:de027e28c171d3da529689cbfa023b0b4eab188c8dfa22fd834eebd2cee6e7bb", size = 21392, upload-time = "2026-07-12T05:14:19.616Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -2318,6 +2330,7 @@ dev = [ { name = "towncrier" }, { name = "ty" }, { name = "types-pyyaml" }, + { name = "types-requests" }, { name = "vale" }, { name = "vulture" }, { name = "vws-python-mock" }, @@ -2381,6 +2394,7 @@ requires-dist = [ { name = "towncrier", marker = "extra == 'release'", specifier = "==26.9.0" }, { name = "ty", marker = "extra == 'dev'", specifier = "==0.0.78" }, { name = "types-pyyaml", marker = "extra == 'dev'", specifier = "==6.0.12.20260815" }, + { name = "types-requests", marker = "extra == 'dev'", specifier = "==2.33.0.20260712" }, { name = "vale", marker = "extra == 'dev'", specifier = "==3.20.0.0" }, { name = "vulture", marker = "extra == 'dev'", specifier = "==2.16" }, { name = "vws-python", specifier = "==2026.8.26" }, From 88f2af998ccd5e3ce9182f2416ffea2c72e01efe Mon Sep 17 00:00:00 2001 From: Adam Dangoor Date: Tue, 8 Sep 2026 10:39:17 +0100 Subject: [PATCH 2/2] Cover pyrefly compatibility branches --- src/vws_cli/options/_types.py | 2 +- tests/test_error_handling.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/vws_cli/options/_types.py b/src/vws_cli/options/_types.py index 6ed39dc3..a52cf517 100644 --- a/src/vws_cli/options/_types.py +++ b/src/vws_cli/options/_types.py @@ -14,4 +14,4 @@ def __call__[**P, R]( /, ) -> Callable[P, R]: """Decorate ``command`` without changing its signature.""" - raise NotImplementedError + raise NotImplementedError # pragma: no cover diff --git a/tests/test_error_handling.py b/tests/test_error_handling.py index e2167c85..1fd22fc0 100644 --- a/tests/test_error_handling.py +++ b/tests/test_error_handling.py @@ -8,11 +8,31 @@ from mock_vws import MockVWS, VuMarkGenerationFailure from mock_vws.database import CloudDatabase from vws import VWS +from vws.exceptions.model_target_exceptions import ModelTargetValidationError +from vws.response import Response from vws_cli import vws_group +from vws_cli._error_handling import get_model_target_error_message from vws_cli.vumark import generate_vumark +def test_model_target_validation_error_without_details() -> None: + """The top-level validation message is used without detail items.""" + response = Response( + text='{"error": {"message": "invalid dataset"}}', + url="https://example.com/model-targets", + status_code=400, + headers={}, + request_body=None, + tell_position=0, + content=b"", + ) + message = get_model_target_error_message( + exc=ModelTargetValidationError(response=response), + ) + assert message == "Error: Vuforia rejected the request.\ninvalid dataset" + + @pytest.mark.parametrize( argnames=("failure", "expected_message"), argvalues=[