From c42915088ee4bbdbc962809785086db0148391ca Mon Sep 17 00:00:00 2001 From: Mustafa Amir Khokhar Date: Tue, 23 Jun 2026 13:14:28 +0500 Subject: [PATCH 1/4] test: type-check test/utils and add it to mypy config Enable mypy type checking for the test/utils directory (part of #10396) as a first piece-meal step toward typing the whole test suite. Fix the errors mypy surfaced across 11 files using real annotations where possible (annotating expected values, adding return types, narrowing float | None with asserts, annotating mock parameters). Where a test intentionally passes invalid input (inside pytest.raises) or monkeypatches a method, a scoped and commented type: ignore is used. No test behaviour is changed. Add test/utils/ to the test:types mypy paths in pyproject.toml. --- pyproject.toml | 2 +- test/utils/test_auth.py | 15 ++++----- test/utils/test_callable_serialization.py | 2 +- test/utils/test_device.py | 2 +- test/utils/test_experimental.py | 2 +- test/utils/test_hf.py | 8 +++-- test/utils/test_http_client.py | 21 +++++++------ test/utils/test_jinja2_chat_extension.py | 37 +++++++++++++++-------- test/utils/test_jinja2_extensions.py | 4 +-- test/utils/test_misc.py | 3 ++ test/utils/test_requests_utils.py | 10 +++--- test/utils/test_type_serialization.py | 17 ++++++----- 12 files changed, 74 insertions(+), 49 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index afbab59cd8c..b142794c6c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -163,7 +163,7 @@ integration-only-slow = 'pytest --maxfail=5 -m "integration and slow" {args:test all = 'pytest {args:test}' # TODO We want to eventually type the whole test folder -types = "mypy --install-types --non-interactive --cache-dir=.mypy_cache/ {args:haystack test/core/ test/marshal/ test/testing/ test/tracing/ test/tools/ test/human_in_the_loop test/evaluation test/document_stores test/dataclasses}" +types = "mypy --install-types --non-interactive --cache-dir=.mypy_cache/ {args:haystack test/core/ test/marshal/ test/testing/ test/tracing/ test/tools/ test/human_in_the_loop test/evaluation test/document_stores test/dataclasses test/utils/}" [tool.hatch.envs.e2e] template = "test" diff --git a/test/utils/test_auth.py b/test/utils/test_auth.py index a7b40424ffe..1c5b40b3517 100644 --- a/test/utils/test_auth.py +++ b/test/utils/test_auth.py @@ -32,9 +32,9 @@ def test_token_secret(): Secret.from_token("") with pytest.raises(FrozenInstanceError): - secret._token = "abba" + secret._token = "abba" # type: ignore[misc] # test sets internal state with pytest.raises(FrozenInstanceError): - secret._type = SecretType.ENV_VAR + secret._type = SecretType.ENV_VAR # type: ignore[misc] # test sets internal state def test_env_var_secret(): @@ -52,11 +52,12 @@ def test_env_var_secret(): secret.resolve_value() secret = Secret.from_env_var("TEST_ENV_VAR2", strict=False) - assert secret._strict is False + assert secret._strict is False # type: ignore[attr-defined] # inspecting internals in test assert secret.resolve_value() is None secret = Secret.from_env_var(["TEST_ENV_VAR2", "TEST_ENV_VAR1"], strict=True) - assert secret._env_vars == ("TEST_ENV_VAR2", "TEST_ENV_VAR1") + # inspecting internals in test (_env_vars exists on the EnvVarSecret subclass) + assert secret._env_vars == ("TEST_ENV_VAR2", "TEST_ENV_VAR1") # type: ignore[attr-defined] with pytest.raises(ValueError, match="None of the following .* variables are set"): secret.resolve_value() os.environ["TEST_ENV_VAR1"] = "test-token-2" @@ -73,8 +74,8 @@ def test_env_var_secret(): ) with pytest.raises(FrozenInstanceError): - secret._env_vars = ("A", "B") + secret._env_vars = ("A", "B") # type: ignore[attr-defined] # inspecting internals in test with pytest.raises(FrozenInstanceError): - secret._strict = False + secret._strict = False # type: ignore[attr-defined] # inspecting internals in test with pytest.raises(FrozenInstanceError): - secret._type = SecretType.TOKEN + secret._type = SecretType.TOKEN # type: ignore[attr-defined] # inspecting internals in test diff --git a/test/utils/test_callable_serialization.py b/test/utils/test_callable_serialization.py index c39acafe655..3f007f45ad3 100644 --- a/test/utils/test_callable_serialization.py +++ b/test/utils/test_callable_serialization.py @@ -11,7 +11,7 @@ from haystack.utils import deserialize_callable, serialize_callable -def some_random_callable_for_testing(some_ignored_arg: str): +def some_random_callable_for_testing(some_ignored_arg: str) -> None: pass diff --git a/test/utils/test_device.py b/test/utils/test_device.py index abd74827d2d..36d897e9d60 100644 --- a/test/utils/test_device.py +++ b/test/utils/test_device.py @@ -57,7 +57,7 @@ def test_device_map(): ) with pytest.raises(TypeError, match="unexpected device"): - DeviceMap.from_hf({"layer1": 0.1}) + DeviceMap.from_hf({"layer1": 0.1}) # type: ignore[dict-item] # intentional invalid input to trigger TypeError assert device_map.first_device == Device.gpu(0) assert DeviceMap({}).first_device is None diff --git a/test/utils/test_experimental.py b/test/utils/test_experimental.py index 257d3252ace..e44522a5300 100644 --- a/test/utils/test_experimental.py +++ b/test/utils/test_experimental.py @@ -39,7 +39,7 @@ class MyComponent: def run(self, value: int) -> dict: return {"value": value} - assert MyComponent.__experimental__ is True + assert getattr(MyComponent, "__experimental__") is True # noqa: B009 # dynamically added by decorator def test_passes_args_and_kwargs_to_init(self): @_experimental diff --git a/test/utils/test_hf.py b/test/utils/test_hf.py index c3e12b9f4e3..47aa59be4ff 100644 --- a/test/utils/test_hf.py +++ b/test/utils/test_hf.py @@ -61,12 +61,14 @@ def test_convert_message_to_hf_format(): tool_result = {"weather": "sunny", "temperature": "25"} message = ChatMessage.from_tool( - tool_result=tool_result, origin=ToolCall(id="123", tool_name="weather", arguments={"city": "Paris"}) + tool_result=tool_result, # type: ignore[arg-type] # test verifies dict result passes through + origin=ToolCall(id="123", tool_name="weather", arguments={"city": "Paris"}), ) assert convert_message_to_hf_format(message) == {"role": "tool", "content": tool_result, "tool_call_id": "123"} message = ChatMessage.from_tool( - tool_result=tool_result, origin=ToolCall(tool_name="weather", arguments={"city": "Paris"}) + tool_result=tool_result, # type: ignore[arg-type] # test verifies dict result passes through + origin=ToolCall(tool_name="weather", arguments={"city": "Paris"}), ) assert convert_message_to_hf_format(message) == {"role": "tool", "content": tool_result} @@ -83,7 +85,7 @@ def test_convert_message_to_hf_invalid(): ToolCallResult( result="result!", origin=ToolCall(id="123", tool_name="weather", arguments={"city": "Paris"}), - error=None, + error=None, # type: ignore[arg-type] # intentionally invalid input ), ], ) diff --git a/test/utils/test_http_client.py b/test/utils/test_http_client.py index 8c52c265b2a..6e7f4bafc70 100644 --- a/test/utils/test_http_client.py +++ b/test/utils/test_http_client.py @@ -9,20 +9,23 @@ def test_init_http_client(): - # test without any params - http_client = init_http_client() + # test without any params (relies on the implementation's default args, which the overloads do not expose) + http_client = init_http_client() # type: ignore[call-overload] # valid no-arg call; overloads omit defaults assert http_client is None - # test client is initialized with http_client_kwargs - http_client = init_http_client(http_client_kwargs={"base_url": "https://example.com"}) + # test client is initialized with http_client_kwargs (async_client correctly defaults to False at runtime, + # but every overload requires it to be passed explicitly) + client_kwargs = {"base_url": "https://example.com"} + http_client = init_http_client(http_client_kwargs=client_kwargs) # type: ignore[call-overload] assert http_client is not None assert isinstance(http_client, httpx.Client) assert http_client.base_url == "https://example.com" def test_init_http_client_async(): - # test without any params - http_async_client = init_http_client(async_client=True) + # test without any params (http_client_kwargs correctly defaults to None at runtime, + # but every overload requires it to be passed explicitly) + http_async_client = init_http_client(async_client=True) # type: ignore[call-overload] assert http_async_client is None # test async client is initialized with http_client_kwargs @@ -35,13 +38,13 @@ def test_init_http_client_async(): def test_http_client_kwargs_type_validation(): # test http_client_kwargs is not a dictionary with pytest.raises(TypeError, match="The parameter 'http_client_kwargs' must be a dictionary."): - init_http_client(http_client_kwargs="invalid") + init_http_client(http_client_kwargs="invalid") # type: ignore[call-overload] # intentionally invalid type def test_http_client_kwargs_with_invalid_params(): - # test http_client_kwargs with invalid keys + # test http_client_kwargs with invalid keys (async_client defaults to False at runtime, but overloads require it) with pytest.raises(TypeError, match="unexpected keyword argument"): - init_http_client(http_client_kwargs={"invalid_key": "invalid"}) + init_http_client(http_client_kwargs={"invalid_key": "invalid"}) # type: ignore[call-overload] def test_init_http_client_with_dict_limits(): diff --git a/test/utils/test_jinja2_chat_extension.py b/test/utils/test_jinja2_chat_extension.py index 4fa17fb8fdb..a7c9c5042b6 100644 --- a/test/utils/test_jinja2_chat_extension.py +++ b/test/utils/test_jinja2_chat_extension.py @@ -4,6 +4,7 @@ import base64 import json +from typing import Any from unittest.mock import patch import pytest @@ -93,7 +94,12 @@ def test_system_message(self, jinja_env): """ rendered = jinja_env.from_string(template).render() output = json.loads(rendered.strip()) - expected = {"role": "system", "content": [{"text": "You are a helpful assistant."}], "name": None, "meta": {}} + expected: dict[str, Any] = { + "role": "system", + "content": [{"text": "You are a helpful assistant."}], + "name": None, + "meta": {}, + } assert output == expected def test_user_message_with_variable(self, jinja_env): @@ -104,7 +110,12 @@ def test_user_message_with_variable(self, jinja_env): """ rendered = jinja_env.from_string(template).render(name="Alice") output = json.loads(rendered.strip()) - expected = {"role": "user", "content": [{"text": "Hello, my name is Alice!"}], "name": None, "meta": {}} + expected: dict[str, Any] = { + "role": "user", + "content": [{"text": "Hello, my name is Alice!"}], + "name": None, + "meta": {}, + } assert output == expected def test_assistant_message_with_tool_call(self, jinja_env): @@ -117,7 +128,7 @@ def test_assistant_message_with_tool_call(self, jinja_env): tool_call = ToolCall(tool_name="search", arguments={"query": "an interesting question"}, id="search_1") rendered = jinja_env.from_string(template).render(tool_call=tool_call) output = json.loads(rendered.strip()) - expected = { + expected: dict[str, Any] = { "role": "assistant", "content": [ {"text": "Let me search for that information."}, @@ -145,7 +156,7 @@ def test_assistant_message_with_reasoning(self, jinja_env): reasoning = ReasoningContent(reasoning_text="Let me think about it...", extra={"key": "value"}) rendered = jinja_env.from_string(template).render(reasoning=reasoning) output = json.loads(rendered.strip()) - expected = { + expected: dict[str, Any] = { "role": "assistant", "content": [ {"reasoning": {"reasoning_text": "Let me think about it...", "extra": {"key": "value"}}}, @@ -166,7 +177,7 @@ def test_tool_message(self, jinja_env): tool_result = ToolCallResult(result="Here are the search results", origin=tool_call, error=False) rendered = jinja_env.from_string(template).render(tool_result=tool_result) output = json.loads(rendered.strip()) - expected = { + expected: dict[str, Any] = { "role": "tool", "content": [ { @@ -204,7 +215,7 @@ def test_tool_message_tool_call_result_list(self, jinja_env, base64_image_string ) rendered = jinja_env.from_string(template).render(tool_result=tool_result) output = json.loads(rendered.strip()) - expected = { + expected: dict[str, Any] = { "role": "tool", "content": [ { @@ -246,7 +257,7 @@ def test_user_message_with_image(self, jinja_env, base64_image_string): image = ImageContent(base64_image=base64_image_string, mime_type="image/png") rendered = jinja_env.from_string(template).render(image=image) output = json.loads(rendered.strip()) - expected = { + expected: dict[str, Any] = { "role": "user", "content": [ {"text": "Please describe this image:"}, @@ -280,7 +291,7 @@ def test_user_message_with_multiple_images(self, jinja_env, base64_image_string) ] rendered = jinja_env.from_string(template).render(images=images) output = json.loads(rendered.strip()) - expected = { + expected: dict[str, Any] = { "role": "user", "content": [ {"text": "Compare these images:"}, @@ -327,7 +338,7 @@ def test_user_message_with_multiple_images_and_interleaved_text(self, jinja_env, rendered = jinja_env.from_string(template).render(images=[image, image]) output = json.loads(rendered.strip()) - expected = { + expected: dict[str, Any] = { "role": "user", "content": [ {"text": "Image 1:"}, @@ -368,7 +379,7 @@ def test_user_message_with_file_content(self, jinja_env, base64_pdf_string): rendered = jinja_env.from_string(template).render(file=file) output = json.loads(rendered.strip()) - expected = { + expected: dict[str, Any] = { "role": "user", "content": [ {"text": "Please describe this document:"}, @@ -398,7 +409,7 @@ def test_user_message_multiple_lines(self, jinja_env): """ rendered = jinja_env.from_string(template).render() output = json.loads(rendered.strip()) - expected = { + expected: dict[str, Any] = { "role": "user", "content": [ { @@ -424,7 +435,7 @@ def test_invalid_role(self, jinja_env): def test_templatize_part_filter_with_invalid_type(self): with pytest.raises(TypeError, match="Unsupported type in ChatMessage content"): - templatize_part(123) + templatize_part(123) # type: ignore[arg-type] # intentionally invalid input to test error handling def test_empty_message_content_raises_error(self, jinja_env): error_message = "Message content in template is empty or contains only whitespace characters." @@ -478,7 +489,7 @@ def test_message_with_whitespace_handling(self, jinja_env): {% endmessage %}""", """{% message role="user" %}\tString\t{% endmessage %}""", ] - expected = {"role": "user", "content": [{"text": "String"}], "name": None, "meta": {}} + expected: dict[str, Any] = {"role": "user", "content": [{"text": "String"}], "name": None, "meta": {}} for template in templates: rendered = jinja_env.from_string(template).render() output = json.loads(rendered.strip()) diff --git a/test/utils/test_jinja2_extensions.py b/test/utils/test_jinja2_extensions.py index 0d1dd09770d..e578d444994 100644 --- a/test/utils/test_jinja2_extensions.py +++ b/test/utils/test_jinja2_extensions.py @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: Apache-2.0 -from unittest.mock import patch +from unittest.mock import MagicMock, patch import arrow import pytest @@ -26,7 +26,7 @@ def jinja_extension(self, jinja_env: Environment) -> Jinja2TimeExtension: return Jinja2TimeExtension(jinja_env) @patch("haystack.utils.jinja2_extensions.arrow_import") - def test_init_fails_without_arrow(self, arrow_import_mock) -> None: + def test_init_fails_without_arrow(self, arrow_import_mock: MagicMock) -> None: arrow_import_mock.check.side_effect = ImportError with pytest.raises(ImportError): Jinja2TimeExtension(Environment()) diff --git a/test/utils/test_misc.py b/test/utils/test_misc.py index 2e241a872dc..4e2286dea8a 100644 --- a/test/utils/test_misc.py +++ b/test/utils/test_misc.py @@ -70,6 +70,7 @@ def test_scores_decrease_with_rank(self): docs = [Document(id="a"), Document(id="b"), Document(id="c")] result = _reciprocal_rank_fusion([docs]) by_id = {doc.id: doc.score for doc in result} + assert by_id["a"] is not None and by_id["b"] is not None and by_id["c"] is not None assert by_id["a"] > by_id["b"] > by_id["c"] def test_deduplicates_across_lists(self): @@ -85,6 +86,7 @@ def test_higher_ranked_doc_gets_higher_score(self): result = _reciprocal_rank_fusion([docs_a, docs_b]) by_id = {doc.id: doc.score for doc in result} # "a" is ranked 1st and 2nd; "c" is ranked 3rd and 1st — "a" should win + assert by_id["a"] is not None and by_id["c"] is not None assert by_id["a"] > by_id["c"] def test_equal_weights_by_default(self): @@ -103,6 +105,7 @@ def test_weights_influence_scores(self): weighted_by_id = {doc.id: doc.score for doc in result_weighted} # with equal weights both docs score the same; with heavy weight on list_a, "a" should outscore "b" assert equal_by_id["a"] == pytest.approx(equal_by_id["b"]) + assert weighted_by_id["a"] is not None and weighted_by_id["b"] is not None assert weighted_by_id["a"] > weighted_by_id["b"] diff --git a/test/utils/test_requests_utils.py b/test/utils/test_requests_utils.py index fd6de9b302d..b07d3662b74 100644 --- a/test/utils/test_requests_utils.py +++ b/test/utils/test_requests_utils.py @@ -104,10 +104,11 @@ def raise_for_status(): "Service Unavailable", request=error_response.request, response=error_response ) - error_response.raise_for_status = raise_for_status + error_response.raise_for_status = raise_for_status # type: ignore[method-assign] # test monkeypatch success_response = httpx.Response(status_code=200, request=httpx.Request("GET", "https://example.com")) - success_response.raise_for_status = lambda: None + # Monkeypatch the method with a stub returning None; the ignore covers the related mypy errors + success_response.raise_for_status = lambda: None # type: ignore[method-assign, assignment, return-value] with patch("httpx.Client.request") as mock_request: # First call returns error status code, second call succeeds @@ -218,10 +219,11 @@ def raise_for_status(): "Service Unavailable", request=error_response.request, response=error_response ) - error_response.raise_for_status = raise_for_status + error_response.raise_for_status = raise_for_status # type: ignore[method-assign] # test monkeypatch success_response = httpx.Response(status_code=200, request=httpx.Request("GET", "https://example.com")) - success_response.raise_for_status = lambda: None + # Monkeypatch the method with a stub returning None; the ignore covers the related mypy errors + success_response.raise_for_status = lambda: None # type: ignore[method-assign, assignment, return-value] with patch("httpx.AsyncClient.request") as mock_request: # First call returns error status code, second call succeeds diff --git a/test/utils/test_type_serialization.py b/test/utils/test_type_serialization.py index b4b7536517b..62cff1b90ba 100644 --- a/test/utils/test_type_serialization.py +++ b/test/utils/test_type_serialization.py @@ -148,7 +148,7 @@ def test_output_type_deserialization(): assert deserialize_type("float") == float assert deserialize_type("bool") == bool assert deserialize_type("None") is None - assert deserialize_type("NoneType") == type(None) # type: ignore + assert deserialize_type("NoneType") == type(None) def test_output_builtin_type_deserialization(): @@ -198,10 +198,12 @@ def test_output_type_deserialization_nested(): def test_output_type_serialization_typing_generic_with_nonetype(): # NoneType used as a regular argument of a typing generic (not the implicit None of Optional) # must be kept, otherwise the serialized type is malformed (e.g. "typing.Dict[str]") or loses information. - assert serialize_type(Dict[str, type(None)]) == "typing.Dict[str, None]" - assert serialize_type(Dict[type(None), str]) == "typing.Dict[None, str]" + # `None` is used here as a regular type argument; at runtime `typing` normalizes it to `NoneType`, + # so `Dict[str, None]` is the same object as `Dict[str, type(None)]` while remaining a valid type for mypy. + assert serialize_type(Dict[str, None]) == "typing.Dict[str, None]" + assert serialize_type(Dict[None, str]) == "typing.Dict[None, str]" assert serialize_type(Tuple[int, type(None)]) == "typing.Tuple[int, None]" - assert serialize_type(List[type(None)]) == "typing.List[None]" + assert serialize_type(List[None]) == "typing.List[None]" # A Union with more than two members that includes None must keep None as well. assert serialize_type(Union[str, int, None]) == "typing.Union[str, int, None]" # Optional must still be serialized without a redundant trailing None. @@ -210,10 +212,11 @@ def test_output_type_serialization_typing_generic_with_nonetype(): def test_output_type_round_trip_typing_generic_with_nonetype(): for type_ in [ - Dict[str, type(None)], - Dict[type(None), str], + # `None` here is normalized to `NoneType` at runtime, identical to `type(None)`, but valid for mypy. + Dict[str, None], + Dict[None, str], Tuple[int, type(None)], - List[type(None)], + List[None], Union[str, int, None], Optional[str], ]: From 5aa675f0a986688822466be4687ab4a44a8b90ae Mon Sep 17 00:00:00 2001 From: Mustafa Amir Khokhar Date: Wed, 24 Jun 2026 21:53:24 +0500 Subject: [PATCH 2/4] style: tidy type: ignore comments in test/utils to match codebase convention --- test/utils/test_auth.py | 13 ++++++------- test/utils/test_device.py | 2 +- test/utils/test_experimental.py | 2 +- test/utils/test_hf.py | 6 +++--- test/utils/test_http_client.py | 14 ++++++-------- test/utils/test_jinja2_chat_extension.py | 2 +- test/utils/test_requests_utils.py | 6 ++---- test/utils/test_type_serialization.py | 3 --- 8 files changed, 20 insertions(+), 28 deletions(-) diff --git a/test/utils/test_auth.py b/test/utils/test_auth.py index 1c5b40b3517..0cded942919 100644 --- a/test/utils/test_auth.py +++ b/test/utils/test_auth.py @@ -32,9 +32,9 @@ def test_token_secret(): Secret.from_token("") with pytest.raises(FrozenInstanceError): - secret._token = "abba" # type: ignore[misc] # test sets internal state + secret._token = "abba" # type: ignore[misc] with pytest.raises(FrozenInstanceError): - secret._type = SecretType.ENV_VAR # type: ignore[misc] # test sets internal state + secret._type = SecretType.ENV_VAR # type: ignore[misc] def test_env_var_secret(): @@ -52,11 +52,10 @@ def test_env_var_secret(): secret.resolve_value() secret = Secret.from_env_var("TEST_ENV_VAR2", strict=False) - assert secret._strict is False # type: ignore[attr-defined] # inspecting internals in test + assert secret._strict is False # type: ignore[attr-defined] assert secret.resolve_value() is None secret = Secret.from_env_var(["TEST_ENV_VAR2", "TEST_ENV_VAR1"], strict=True) - # inspecting internals in test (_env_vars exists on the EnvVarSecret subclass) assert secret._env_vars == ("TEST_ENV_VAR2", "TEST_ENV_VAR1") # type: ignore[attr-defined] with pytest.raises(ValueError, match="None of the following .* variables are set"): secret.resolve_value() @@ -74,8 +73,8 @@ def test_env_var_secret(): ) with pytest.raises(FrozenInstanceError): - secret._env_vars = ("A", "B") # type: ignore[attr-defined] # inspecting internals in test + secret._env_vars = ("A", "B") # type: ignore[attr-defined] with pytest.raises(FrozenInstanceError): - secret._strict = False # type: ignore[attr-defined] # inspecting internals in test + secret._strict = False # type: ignore[attr-defined] with pytest.raises(FrozenInstanceError): - secret._type = SecretType.TOKEN # type: ignore[attr-defined] # inspecting internals in test + secret._type = SecretType.TOKEN # type: ignore[attr-defined] diff --git a/test/utils/test_device.py b/test/utils/test_device.py index 36d897e9d60..2d7c1b7d889 100644 --- a/test/utils/test_device.py +++ b/test/utils/test_device.py @@ -57,7 +57,7 @@ def test_device_map(): ) with pytest.raises(TypeError, match="unexpected device"): - DeviceMap.from_hf({"layer1": 0.1}) # type: ignore[dict-item] # intentional invalid input to trigger TypeError + DeviceMap.from_hf({"layer1": 0.1}) # type: ignore[dict-item] assert device_map.first_device == Device.gpu(0) assert DeviceMap({}).first_device is None diff --git a/test/utils/test_experimental.py b/test/utils/test_experimental.py index e44522a5300..627f241ad87 100644 --- a/test/utils/test_experimental.py +++ b/test/utils/test_experimental.py @@ -39,7 +39,7 @@ class MyComponent: def run(self, value: int) -> dict: return {"value": value} - assert getattr(MyComponent, "__experimental__") is True # noqa: B009 # dynamically added by decorator + assert getattr(MyComponent, "__experimental__") is True # noqa: B009 def test_passes_args_and_kwargs_to_init(self): @_experimental diff --git a/test/utils/test_hf.py b/test/utils/test_hf.py index 47aa59be4ff..2b7773f619e 100644 --- a/test/utils/test_hf.py +++ b/test/utils/test_hf.py @@ -61,13 +61,13 @@ def test_convert_message_to_hf_format(): tool_result = {"weather": "sunny", "temperature": "25"} message = ChatMessage.from_tool( - tool_result=tool_result, # type: ignore[arg-type] # test verifies dict result passes through + tool_result=tool_result, # type: ignore[arg-type] origin=ToolCall(id="123", tool_name="weather", arguments={"city": "Paris"}), ) assert convert_message_to_hf_format(message) == {"role": "tool", "content": tool_result, "tool_call_id": "123"} message = ChatMessage.from_tool( - tool_result=tool_result, # type: ignore[arg-type] # test verifies dict result passes through + tool_result=tool_result, # type: ignore[arg-type] origin=ToolCall(tool_name="weather", arguments={"city": "Paris"}), ) assert convert_message_to_hf_format(message) == {"role": "tool", "content": tool_result} @@ -85,7 +85,7 @@ def test_convert_message_to_hf_invalid(): ToolCallResult( result="result!", origin=ToolCall(id="123", tool_name="weather", arguments={"city": "Paris"}), - error=None, # type: ignore[arg-type] # intentionally invalid input + error=None, # type: ignore[arg-type] ), ], ) diff --git a/test/utils/test_http_client.py b/test/utils/test_http_client.py index 6e7f4bafc70..0cc05d9d047 100644 --- a/test/utils/test_http_client.py +++ b/test/utils/test_http_client.py @@ -9,12 +9,11 @@ def test_init_http_client(): - # test without any params (relies on the implementation's default args, which the overloads do not expose) - http_client = init_http_client() # type: ignore[call-overload] # valid no-arg call; overloads omit defaults + # test without any params + http_client = init_http_client() # type: ignore[call-overload] assert http_client is None - # test client is initialized with http_client_kwargs (async_client correctly defaults to False at runtime, - # but every overload requires it to be passed explicitly) + # test client is initialized with http_client_kwargs client_kwargs = {"base_url": "https://example.com"} http_client = init_http_client(http_client_kwargs=client_kwargs) # type: ignore[call-overload] assert http_client is not None @@ -23,8 +22,7 @@ def test_init_http_client(): def test_init_http_client_async(): - # test without any params (http_client_kwargs correctly defaults to None at runtime, - # but every overload requires it to be passed explicitly) + # test without any params http_async_client = init_http_client(async_client=True) # type: ignore[call-overload] assert http_async_client is None @@ -38,11 +36,11 @@ def test_init_http_client_async(): def test_http_client_kwargs_type_validation(): # test http_client_kwargs is not a dictionary with pytest.raises(TypeError, match="The parameter 'http_client_kwargs' must be a dictionary."): - init_http_client(http_client_kwargs="invalid") # type: ignore[call-overload] # intentionally invalid type + init_http_client(http_client_kwargs="invalid") # type: ignore[call-overload] def test_http_client_kwargs_with_invalid_params(): - # test http_client_kwargs with invalid keys (async_client defaults to False at runtime, but overloads require it) + # test http_client_kwargs with invalid keys with pytest.raises(TypeError, match="unexpected keyword argument"): init_http_client(http_client_kwargs={"invalid_key": "invalid"}) # type: ignore[call-overload] diff --git a/test/utils/test_jinja2_chat_extension.py b/test/utils/test_jinja2_chat_extension.py index c6fff08eb48..0c2ec086d96 100644 --- a/test/utils/test_jinja2_chat_extension.py +++ b/test/utils/test_jinja2_chat_extension.py @@ -439,7 +439,7 @@ def test_invalid_role(self, jinja_env): def test_templatize_part_filter_with_invalid_type(self, jinja_env): with pytest.raises(TypeError, match="Unsupported type in ChatMessage content"): - templatize_part(jinja_env, 123) # type: ignore[arg-type] # intentionally invalid input + templatize_part(jinja_env, 123) # type: ignore[arg-type] def test_empty_message_content_raises_error(self, jinja_env): error_message = "Message content in template is empty or contains only whitespace characters." diff --git a/test/utils/test_requests_utils.py b/test/utils/test_requests_utils.py index b07d3662b74..9bf621268ae 100644 --- a/test/utils/test_requests_utils.py +++ b/test/utils/test_requests_utils.py @@ -104,10 +104,9 @@ def raise_for_status(): "Service Unavailable", request=error_response.request, response=error_response ) - error_response.raise_for_status = raise_for_status # type: ignore[method-assign] # test monkeypatch + error_response.raise_for_status = raise_for_status # type: ignore[method-assign] success_response = httpx.Response(status_code=200, request=httpx.Request("GET", "https://example.com")) - # Monkeypatch the method with a stub returning None; the ignore covers the related mypy errors success_response.raise_for_status = lambda: None # type: ignore[method-assign, assignment, return-value] with patch("httpx.Client.request") as mock_request: @@ -219,10 +218,9 @@ def raise_for_status(): "Service Unavailable", request=error_response.request, response=error_response ) - error_response.raise_for_status = raise_for_status # type: ignore[method-assign] # test monkeypatch + error_response.raise_for_status = raise_for_status # type: ignore[method-assign] success_response = httpx.Response(status_code=200, request=httpx.Request("GET", "https://example.com")) - # Monkeypatch the method with a stub returning None; the ignore covers the related mypy errors success_response.raise_for_status = lambda: None # type: ignore[method-assign, assignment, return-value] with patch("httpx.AsyncClient.request") as mock_request: diff --git a/test/utils/test_type_serialization.py b/test/utils/test_type_serialization.py index 62cff1b90ba..bc2367681b5 100644 --- a/test/utils/test_type_serialization.py +++ b/test/utils/test_type_serialization.py @@ -198,8 +198,6 @@ def test_output_type_deserialization_nested(): def test_output_type_serialization_typing_generic_with_nonetype(): # NoneType used as a regular argument of a typing generic (not the implicit None of Optional) # must be kept, otherwise the serialized type is malformed (e.g. "typing.Dict[str]") or loses information. - # `None` is used here as a regular type argument; at runtime `typing` normalizes it to `NoneType`, - # so `Dict[str, None]` is the same object as `Dict[str, type(None)]` while remaining a valid type for mypy. assert serialize_type(Dict[str, None]) == "typing.Dict[str, None]" assert serialize_type(Dict[None, str]) == "typing.Dict[None, str]" assert serialize_type(Tuple[int, type(None)]) == "typing.Tuple[int, None]" @@ -212,7 +210,6 @@ def test_output_type_serialization_typing_generic_with_nonetype(): def test_output_type_round_trip_typing_generic_with_nonetype(): for type_ in [ - # `None` here is normalized to `NoneType` at runtime, identical to `type(None)`, but valid for mypy. Dict[str, None], Dict[None, str], Tuple[int, type(None)], From fa23a66ad2ee8b3d5eb4940fef02e332276998b1 Mon Sep 17 00:00:00 2001 From: Mustafa Amir Khokhar Date: Wed, 1 Jul 2026 11:44:52 +0500 Subject: [PATCH 3/4] fix: correct init_http_client overloads and drop masking type-ignores Address review feedback on the test/utils type-checking work: - Fix the init_http_client overloads to carry defaults, so calling it with no/partial arguments matches an overload; remove the now-unnecessary call-overload ignores in the test. - test_auth: narrow with assert isinstance(secret, EnvVarSecret) instead of ignoring attr-defined on private-attribute access. - test_hf: pass a correctly-typed str tool_result instead of a dict, removing the arg-type ignores. --- haystack/utils/http_client.py | 12 ++---------- test/utils/test_auth.py | 12 +++++++----- test/utils/test_hf.py | 8 +++----- test/utils/test_http_client.py | 8 ++++---- 4 files changed, 16 insertions(+), 24 deletions(-) diff --git a/haystack/utils/http_client.py b/haystack/utils/http_client.py index 61d323172c3..7b198e4c4a6 100644 --- a/haystack/utils/http_client.py +++ b/haystack/utils/http_client.py @@ -7,22 +7,14 @@ import httpx -@overload -def init_http_client(http_client_kwargs: dict[str, Any], async_client: Literal[False]) -> httpx.Client: ... @overload def init_http_client( - http_client_kwargs: dict[str, Any] | None, async_client: Literal[False] + http_client_kwargs: dict[str, Any] | None = ..., async_client: Literal[False] = ... ) -> httpx.Client | None: ... @overload -def init_http_client(http_client_kwargs: dict[str, Any], async_client: Literal[True]) -> httpx.AsyncClient: ... -@overload def init_http_client( - http_client_kwargs: dict[str, Any] | None, async_client: Literal[True] + http_client_kwargs: dict[str, Any] | None = ..., async_client: Literal[True] = ... ) -> httpx.AsyncClient | None: ... -@overload -def init_http_client( - http_client_kwargs: dict[str, Any] | None, async_client: bool -) -> httpx.Client | httpx.AsyncClient | None: ... def init_http_client( http_client_kwargs: dict[str, Any] | None = None, async_client: bool = False ) -> httpx.Client | httpx.AsyncClient | None: diff --git a/test/utils/test_auth.py b/test/utils/test_auth.py index 0cded942919..2445655fe90 100644 --- a/test/utils/test_auth.py +++ b/test/utils/test_auth.py @@ -52,11 +52,13 @@ def test_env_var_secret(): secret.resolve_value() secret = Secret.from_env_var("TEST_ENV_VAR2", strict=False) - assert secret._strict is False # type: ignore[attr-defined] + assert isinstance(secret, EnvVarSecret) + assert secret._strict is False assert secret.resolve_value() is None secret = Secret.from_env_var(["TEST_ENV_VAR2", "TEST_ENV_VAR1"], strict=True) - assert secret._env_vars == ("TEST_ENV_VAR2", "TEST_ENV_VAR1") # type: ignore[attr-defined] + assert isinstance(secret, EnvVarSecret) + assert secret._env_vars == ("TEST_ENV_VAR2", "TEST_ENV_VAR1") with pytest.raises(ValueError, match="None of the following .* variables are set"): secret.resolve_value() os.environ["TEST_ENV_VAR1"] = "test-token-2" @@ -73,8 +75,8 @@ def test_env_var_secret(): ) with pytest.raises(FrozenInstanceError): - secret._env_vars = ("A", "B") # type: ignore[attr-defined] + secret._env_vars = ("A", "B") # type: ignore[misc] with pytest.raises(FrozenInstanceError): - secret._strict = False # type: ignore[attr-defined] + secret._strict = False # type: ignore[misc] with pytest.raises(FrozenInstanceError): - secret._type = SecretType.TOKEN # type: ignore[attr-defined] + secret._type = SecretType.TOKEN # type: ignore[misc] diff --git a/test/utils/test_hf.py b/test/utils/test_hf.py index 2b7773f619e..1c0498d5c00 100644 --- a/test/utils/test_hf.py +++ b/test/utils/test_hf.py @@ -59,16 +59,14 @@ def test_convert_message_to_hf_format(): "tool_calls": [{"type": "function", "function": {"name": "weather", "arguments": {"city": "Paris"}}}], } - tool_result = {"weather": "sunny", "temperature": "25"} + tool_result = '{"weather": "sunny", "temperature": "25"}' message = ChatMessage.from_tool( - tool_result=tool_result, # type: ignore[arg-type] - origin=ToolCall(id="123", tool_name="weather", arguments={"city": "Paris"}), + tool_result=tool_result, origin=ToolCall(id="123", tool_name="weather", arguments={"city": "Paris"}) ) assert convert_message_to_hf_format(message) == {"role": "tool", "content": tool_result, "tool_call_id": "123"} message = ChatMessage.from_tool( - tool_result=tool_result, # type: ignore[arg-type] - origin=ToolCall(tool_name="weather", arguments={"city": "Paris"}), + tool_result=tool_result, origin=ToolCall(tool_name="weather", arguments={"city": "Paris"}) ) assert convert_message_to_hf_format(message) == {"role": "tool", "content": tool_result} diff --git a/test/utils/test_http_client.py b/test/utils/test_http_client.py index 0cc05d9d047..43c4c2496c1 100644 --- a/test/utils/test_http_client.py +++ b/test/utils/test_http_client.py @@ -10,12 +10,12 @@ def test_init_http_client(): # test without any params - http_client = init_http_client() # type: ignore[call-overload] + http_client = init_http_client() assert http_client is None # test client is initialized with http_client_kwargs client_kwargs = {"base_url": "https://example.com"} - http_client = init_http_client(http_client_kwargs=client_kwargs) # type: ignore[call-overload] + http_client = init_http_client(http_client_kwargs=client_kwargs) assert http_client is not None assert isinstance(http_client, httpx.Client) assert http_client.base_url == "https://example.com" @@ -23,7 +23,7 @@ def test_init_http_client(): def test_init_http_client_async(): # test without any params - http_async_client = init_http_client(async_client=True) # type: ignore[call-overload] + http_async_client = init_http_client(async_client=True) assert http_async_client is None # test async client is initialized with http_client_kwargs @@ -42,7 +42,7 @@ def test_http_client_kwargs_type_validation(): def test_http_client_kwargs_with_invalid_params(): # test http_client_kwargs with invalid keys with pytest.raises(TypeError, match="unexpected keyword argument"): - init_http_client(http_client_kwargs={"invalid_key": "invalid"}) # type: ignore[call-overload] + init_http_client(http_client_kwargs={"invalid_key": "invalid"}) def test_init_http_client_with_dict_limits(): From f453cfe638e989cde00b80938f946c41bf30d0de Mon Sep 17 00:00:00 2001 From: Mustafa Amir Khokhar Date: Wed, 1 Jul 2026 13:02:29 +0500 Subject: [PATCH 4/4] test: keep type(None) in serialization tests per review Revert the type(None) -> None change in test_type_serialization: those tests specifically verify NoneType as a typing-generic argument, so keep type(None) and use a scoped # type: ignore[misc] instead (intent-preserving fix). --- test/utils/test_type_serialization.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/utils/test_type_serialization.py b/test/utils/test_type_serialization.py index bc2367681b5..913e22538fc 100644 --- a/test/utils/test_type_serialization.py +++ b/test/utils/test_type_serialization.py @@ -198,10 +198,10 @@ def test_output_type_deserialization_nested(): def test_output_type_serialization_typing_generic_with_nonetype(): # NoneType used as a regular argument of a typing generic (not the implicit None of Optional) # must be kept, otherwise the serialized type is malformed (e.g. "typing.Dict[str]") or loses information. - assert serialize_type(Dict[str, None]) == "typing.Dict[str, None]" - assert serialize_type(Dict[None, str]) == "typing.Dict[None, str]" + assert serialize_type(Dict[str, type(None)]) == "typing.Dict[str, None]" # type: ignore[misc] + assert serialize_type(Dict[type(None), str]) == "typing.Dict[None, str]" # type: ignore[misc] assert serialize_type(Tuple[int, type(None)]) == "typing.Tuple[int, None]" - assert serialize_type(List[None]) == "typing.List[None]" + assert serialize_type(List[type(None)]) == "typing.List[None]" # type: ignore[misc] # A Union with more than two members that includes None must keep None as well. assert serialize_type(Union[str, int, None]) == "typing.Union[str, int, None]" # Optional must still be serialized without a redundant trailing None. @@ -210,10 +210,10 @@ def test_output_type_serialization_typing_generic_with_nonetype(): def test_output_type_round_trip_typing_generic_with_nonetype(): for type_ in [ - Dict[str, None], - Dict[None, str], + Dict[str, type(None)], # type: ignore[misc] + Dict[type(None), str], # type: ignore[misc] Tuple[int, type(None)], - List[None], + List[type(None)], # type: ignore[misc] Union[str, int, None], Optional[str], ]: