hud-python 0.4.51__py3-none-any.whl → 0.4.53__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of hud-python might be problematic. Click here for more details.

Files changed (88) hide show
  1. hud/__init__.py +13 -1
  2. hud/agents/base.py +14 -3
  3. hud/agents/lite_llm.py +1 -1
  4. hud/agents/openai_chat_generic.py +15 -3
  5. hud/agents/tests/test_base.py +9 -2
  6. hud/agents/tests/test_base_runtime.py +164 -0
  7. hud/cli/__init__.py +18 -25
  8. hud/cli/build.py +35 -27
  9. hud/cli/dev.py +11 -29
  10. hud/cli/eval.py +114 -145
  11. hud/cli/tests/test_analyze_module.py +120 -0
  12. hud/cli/tests/test_build.py +26 -3
  13. hud/cli/tests/test_build_failure.py +41 -0
  14. hud/cli/tests/test_build_module.py +50 -0
  15. hud/cli/tests/test_cli_more_wrappers.py +30 -0
  16. hud/cli/tests/test_cli_root.py +134 -0
  17. hud/cli/tests/test_eval.py +4 -0
  18. hud/cli/tests/test_mcp_server.py +8 -7
  19. hud/cli/tests/test_push_happy.py +74 -0
  20. hud/cli/tests/test_push_wrapper.py +23 -0
  21. hud/cli/utils/docker.py +120 -1
  22. hud/cli/utils/runner.py +1 -1
  23. hud/cli/utils/tasks.py +4 -1
  24. hud/cli/utils/tests/__init__.py +0 -0
  25. hud/cli/utils/tests/test_config.py +58 -0
  26. hud/cli/utils/tests/test_docker.py +93 -0
  27. hud/cli/utils/tests/test_docker_hints.py +71 -0
  28. hud/cli/utils/tests/test_env_check.py +74 -0
  29. hud/cli/utils/tests/test_environment.py +42 -0
  30. hud/cli/utils/tests/test_interactive_module.py +60 -0
  31. hud/cli/utils/tests/test_local_runner.py +50 -0
  32. hud/cli/utils/tests/test_logging_utils.py +23 -0
  33. hud/cli/utils/tests/test_metadata.py +49 -0
  34. hud/cli/utils/tests/test_package_runner.py +35 -0
  35. hud/cli/utils/tests/test_registry_utils.py +49 -0
  36. hud/cli/utils/tests/test_remote_runner.py +25 -0
  37. hud/cli/utils/tests/test_runner_modules.py +52 -0
  38. hud/cli/utils/tests/test_source_hash.py +36 -0
  39. hud/cli/utils/tests/test_tasks.py +80 -0
  40. hud/cli/utils/version_check.py +257 -0
  41. hud/clients/base.py +1 -1
  42. hud/clients/mcp_use.py +3 -1
  43. hud/datasets/parallel.py +2 -2
  44. hud/datasets/runner.py +85 -24
  45. hud/datasets/tests/__init__.py +0 -0
  46. hud/datasets/tests/test_runner.py +106 -0
  47. hud/datasets/tests/test_utils.py +228 -0
  48. hud/otel/config.py +8 -6
  49. hud/otel/context.py +4 -4
  50. hud/otel/exporters.py +231 -57
  51. hud/otel/tests/__init__.py +0 -1
  52. hud/otel/tests/test_instrumentation.py +207 -0
  53. hud/rl/learner.py +1 -1
  54. hud/server/tests/test_server_extra.py +2 -0
  55. hud/shared/exceptions.py +35 -9
  56. hud/shared/hints.py +25 -0
  57. hud/shared/requests.py +15 -3
  58. hud/shared/tests/test_exceptions.py +39 -30
  59. hud/shared/tests/test_hints.py +167 -0
  60. hud/telemetry/__init__.py +30 -6
  61. hud/telemetry/async_context.py +331 -0
  62. hud/telemetry/job.py +51 -12
  63. hud/telemetry/tests/test_async_context.py +242 -0
  64. hud/telemetry/tests/test_instrument.py +414 -0
  65. hud/telemetry/tests/test_job.py +609 -0
  66. hud/telemetry/tests/test_trace.py +184 -6
  67. hud/telemetry/trace.py +16 -17
  68. hud/tools/computer/qwen.py +4 -1
  69. hud/tools/computer/settings.py +2 -2
  70. hud/tools/executors/base.py +4 -2
  71. hud/tools/tests/test_submit.py +85 -0
  72. hud/tools/tests/test_types.py +193 -0
  73. hud/types.py +7 -1
  74. hud/utils/agent_factories.py +1 -3
  75. hud/utils/mcp.py +1 -1
  76. hud/utils/task_tracking.py +223 -0
  77. hud/utils/tests/test_agent_factories.py +60 -0
  78. hud/utils/tests/test_mcp.py +4 -6
  79. hud/utils/tests/test_pretty_errors.py +186 -0
  80. hud/utils/tests/test_tasks.py +187 -0
  81. hud/utils/tests/test_tool_shorthand.py +154 -0
  82. hud/utils/tests/test_version.py +1 -1
  83. hud/version.py +1 -1
  84. {hud_python-0.4.51.dist-info → hud_python-0.4.53.dist-info}/METADATA +48 -48
  85. {hud_python-0.4.51.dist-info → hud_python-0.4.53.dist-info}/RECORD +88 -47
  86. {hud_python-0.4.51.dist-info → hud_python-0.4.53.dist-info}/WHEEL +0 -0
  87. {hud_python-0.4.51.dist-info → hud_python-0.4.53.dist-info}/entry_points.txt +0 -0
  88. {hud_python-0.4.51.dist-info → hud_python-0.4.53.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,242 @@
1
+ from __future__ import annotations
2
+
3
+ from unittest.mock import AsyncMock, MagicMock, patch
4
+
5
+ import pytest
6
+
7
+ from hud.telemetry.async_context import async_job, async_trace
8
+
9
+
10
+ @pytest.mark.asyncio
11
+ async def test_async_trace_basic():
12
+ """Test basic AsyncTrace usage."""
13
+ with (
14
+ patch("hud.telemetry.async_context.OtelTrace") as mock_otel,
15
+ patch("hud.telemetry.async_context.track_task"),
16
+ patch("hud.telemetry.async_context._print_trace_url"),
17
+ patch("hud.telemetry.async_context._print_trace_complete_url"),
18
+ ):
19
+ mock_otel_instance = MagicMock()
20
+ mock_otel.return_value = mock_otel_instance
21
+
22
+ async with async_trace("Test Task") as trace_obj:
23
+ assert trace_obj.name == "Test Task"
24
+ assert trace_obj.id is not None
25
+
26
+
27
+ @pytest.mark.asyncio
28
+ async def test_async_trace_with_job_id():
29
+ """Test AsyncTrace with job_id parameter."""
30
+ with (
31
+ patch("hud.telemetry.async_context.OtelTrace") as mock_otel,
32
+ patch("hud.telemetry.async_context.track_task"),
33
+ ):
34
+ mock_otel_instance = MagicMock()
35
+ mock_otel.return_value = mock_otel_instance
36
+
37
+ async with async_trace("Test", job_id="job-123") as trace_obj:
38
+ assert trace_obj.job_id == "job-123"
39
+
40
+
41
+ @pytest.mark.asyncio
42
+ async def test_async_trace_with_task_id():
43
+ """Test AsyncTrace with task_id parameter."""
44
+ with (
45
+ patch("hud.telemetry.async_context.OtelTrace") as mock_otel,
46
+ patch("hud.telemetry.async_context.track_task"),
47
+ ):
48
+ mock_otel_instance = MagicMock()
49
+ mock_otel.return_value = mock_otel_instance
50
+
51
+ async with async_trace("Test", task_id="task-456") as trace_obj:
52
+ assert trace_obj.task_id == "task-456"
53
+
54
+
55
+ @pytest.mark.asyncio
56
+ async def test_async_trace_prints_url_without_job():
57
+ """Test AsyncTrace prints URL when not part of a job."""
58
+ with (
59
+ patch("hud.telemetry.async_context.settings") as mock_settings,
60
+ patch("hud.telemetry.async_context.OtelTrace") as mock_otel,
61
+ patch("hud.telemetry.async_context.track_task"),
62
+ patch("hud.telemetry.async_context._print_trace_url") as mock_print_url,
63
+ ):
64
+ mock_settings.telemetry_enabled = True
65
+ mock_settings.api_key = "test-key"
66
+ mock_otel_instance = MagicMock()
67
+ mock_otel.return_value = mock_otel_instance
68
+
69
+ async with async_trace("Test", job_id=None):
70
+ pass
71
+
72
+ # Should print trace URL
73
+ mock_print_url.assert_called_once()
74
+
75
+
76
+ @pytest.mark.asyncio
77
+ async def test_async_trace_no_print_url_with_job():
78
+ """Test AsyncTrace doesn't print URL when part of a job."""
79
+ with (
80
+ patch("hud.telemetry.async_context.settings") as mock_settings,
81
+ patch("hud.telemetry.async_context.OtelTrace") as mock_otel,
82
+ patch("hud.telemetry.async_context.track_task"),
83
+ patch("hud.telemetry.async_context._print_trace_url") as mock_print_url,
84
+ ):
85
+ mock_settings.telemetry_enabled = True
86
+ mock_settings.api_key = "test-key"
87
+ mock_otel_instance = MagicMock()
88
+ mock_otel.return_value = mock_otel_instance
89
+
90
+ async with async_trace("Test", job_id="job-123"):
91
+ pass
92
+
93
+ # Should NOT print trace URL when job_id is set
94
+ mock_print_url.assert_not_called()
95
+
96
+
97
+ @pytest.mark.asyncio
98
+ async def test_async_trace_with_exception():
99
+ """Test AsyncTrace handles exceptions."""
100
+ with (
101
+ patch("hud.telemetry.async_context.settings") as mock_settings,
102
+ patch("hud.telemetry.async_context.OtelTrace") as mock_otel,
103
+ patch("hud.telemetry.async_context.track_task"),
104
+ patch("hud.telemetry.async_context._print_trace_complete_url") as mock_print,
105
+ ):
106
+ # Enable telemetry for this test
107
+ mock_settings.telemetry_enabled = True
108
+ mock_settings.api_key = "test-key"
109
+
110
+ mock_otel_instance = MagicMock()
111
+ mock_otel.return_value = mock_otel_instance
112
+
113
+ with pytest.raises(ValueError):
114
+ async with async_trace("Test"):
115
+ raise ValueError("Test error")
116
+
117
+ # Should have been called with error_occurred keyword arg
118
+ mock_print.assert_called_once()
119
+ call_kwargs = mock_print.call_args[1]
120
+ assert call_kwargs["error_occurred"] is True
121
+
122
+
123
+ @pytest.mark.asyncio
124
+ async def test_async_job_basic():
125
+ """Test basic AsyncJob usage."""
126
+ with (
127
+ patch("hud.telemetry.async_context.track_task"),
128
+ patch("hud.telemetry.async_context._print_job_url"),
129
+ patch("hud.telemetry.async_context._print_job_complete_url"),
130
+ ):
131
+ async with async_job("Test Job") as job_obj:
132
+ assert job_obj.name == "Test Job"
133
+ assert job_obj.id is not None
134
+
135
+
136
+ @pytest.mark.asyncio
137
+ async def test_async_job_with_metadata():
138
+ """Test AsyncJob with metadata."""
139
+ with (
140
+ patch("hud.telemetry.async_context.track_task"),
141
+ patch("hud.telemetry.async_context._print_job_url"),
142
+ patch("hud.telemetry.async_context._print_job_complete_url"),
143
+ ):
144
+ async with async_job("Test", metadata={"key": "value"}) as job_obj:
145
+ assert job_obj.metadata == {"key": "value"}
146
+
147
+
148
+ @pytest.mark.asyncio
149
+ async def test_async_job_with_dataset_link():
150
+ """Test AsyncJob with dataset_link."""
151
+ with (
152
+ patch("hud.telemetry.async_context.track_task"),
153
+ patch("hud.telemetry.async_context._print_job_url"),
154
+ patch("hud.telemetry.async_context._print_job_complete_url"),
155
+ ):
156
+ async with async_job("Test", dataset_link="test/dataset") as job_obj:
157
+ assert job_obj.dataset_link == "test/dataset"
158
+
159
+
160
+ @pytest.mark.asyncio
161
+ async def test_async_job_with_custom_job_id():
162
+ """Test AsyncJob with custom job_id."""
163
+ with (
164
+ patch("hud.telemetry.async_context.track_task"),
165
+ patch("hud.telemetry.async_context._print_job_url"),
166
+ patch("hud.telemetry.async_context._print_job_complete_url"),
167
+ ):
168
+ async with async_job("Test", job_id="custom-id") as job_obj:
169
+ assert job_obj.id == "custom-id"
170
+
171
+
172
+ @pytest.mark.asyncio
173
+ async def test_async_job_with_exception():
174
+ """Test AsyncJob handles exceptions."""
175
+ with (
176
+ patch("hud.telemetry.async_context.track_task"),
177
+ patch("hud.telemetry.async_context._print_job_url"),
178
+ patch("hud.telemetry.async_context._print_job_complete_url") as mock_print,
179
+ ):
180
+ with pytest.raises(ValueError):
181
+ async with async_job("Test"):
182
+ raise ValueError("Job error")
183
+
184
+ # Should print with error_occurred keyword arg
185
+ mock_print.assert_called_once()
186
+ call_kwargs = mock_print.call_args[1]
187
+ assert call_kwargs["error_occurred"] is True
188
+
189
+
190
+ @pytest.mark.asyncio
191
+ async def test_async_job_status_updates():
192
+ """Test AsyncJob sends status updates."""
193
+ with (
194
+ patch("hud.telemetry.async_context.settings") as mock_settings,
195
+ patch("hud.telemetry.async_context.track_task") as mock_track,
196
+ patch("hud.telemetry.async_context._print_job_url"),
197
+ patch("hud.telemetry.async_context._print_job_complete_url"),
198
+ ):
199
+ mock_settings.telemetry_enabled = True
200
+ mock_settings.api_key = "test-key"
201
+ mock_settings.hud_telemetry_url = "https://test.com"
202
+
203
+ async with async_job("Test"):
204
+ pass
205
+
206
+ # Should have called track_task twice (running and completed)
207
+ assert mock_track.call_count == 2
208
+
209
+
210
+ @pytest.mark.asyncio
211
+ async def test_async_job_includes_dataset_link_in_status():
212
+ """Test AsyncJob includes dataset_link in status updates."""
213
+ with (
214
+ patch("hud.telemetry.async_context.settings") as mock_settings,
215
+ patch("hud.telemetry.async_context.track_task"),
216
+ patch("hud.telemetry.async_context.make_request", new_callable=AsyncMock),
217
+ patch("hud.telemetry.async_context._print_job_url"),
218
+ patch("hud.telemetry.async_context._print_job_complete_url"),
219
+ ):
220
+ mock_settings.telemetry_enabled = True
221
+ mock_settings.api_key = "test-key"
222
+ mock_settings.hud_telemetry_url = "https://test.com"
223
+
224
+ async with async_job("Test", dataset_link="test/dataset"):
225
+ pass
226
+
227
+
228
+ @pytest.mark.asyncio
229
+ async def test_async_trace_non_root():
230
+ """Test AsyncTrace with root=False."""
231
+ with (
232
+ patch("hud.telemetry.async_context.OtelTrace") as mock_otel,
233
+ patch("hud.telemetry.async_context.track_task") as mock_track,
234
+ ):
235
+ mock_otel_instance = MagicMock()
236
+ mock_otel.return_value = mock_otel_instance
237
+
238
+ async with async_trace("Test", root=False):
239
+ pass
240
+
241
+ # Should not track status updates for non-root traces
242
+ mock_track.assert_not_called()
@@ -0,0 +1,414 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ import pytest
6
+ from opentelemetry.trace import SpanKind
7
+
8
+ from hud.telemetry.instrument import _serialize_value, instrument
9
+
10
+
11
+ def test_serialize_value_simple_types():
12
+ """Test _serialize_value with simple types."""
13
+ assert _serialize_value("string") == "string"
14
+ assert _serialize_value(42) == 42
15
+ assert _serialize_value(3.14) == 3.14
16
+ assert _serialize_value(True) is True
17
+ assert _serialize_value(None) is None
18
+
19
+
20
+ def test_serialize_value_list():
21
+ """Test _serialize_value with lists."""
22
+ result = _serialize_value([1, 2, 3])
23
+ assert result == [1, 2, 3]
24
+
25
+
26
+ def test_serialize_value_list_truncation():
27
+ """Test _serialize_value truncates long lists."""
28
+ long_list = list(range(20))
29
+ result = _serialize_value(long_list, max_items=5)
30
+ assert len(result) == 5
31
+ assert result == [0, 1, 2, 3, 4]
32
+
33
+
34
+ def test_serialize_value_tuple():
35
+ """Test _serialize_value with tuples."""
36
+ result = _serialize_value((1, 2, 3))
37
+ assert result == [1, 2, 3] # Converted to list by JSON
38
+
39
+
40
+ def test_serialize_value_tuple_truncation():
41
+ """Test _serialize_value truncates long tuples."""
42
+ long_tuple = tuple(range(20))
43
+ result = _serialize_value(long_tuple, max_items=5)
44
+ assert len(result) == 5
45
+
46
+
47
+ def test_serialize_value_dict():
48
+ """Test _serialize_value with dicts."""
49
+ result = _serialize_value({"key": "value"})
50
+ assert result == {"key": "value"}
51
+
52
+
53
+ def test_serialize_value_dict_truncation():
54
+ """Test _serialize_value truncates large dicts."""
55
+ large_dict = {f"key{i}": i for i in range(20)}
56
+ result = _serialize_value(large_dict, max_items=5)
57
+ assert len(result) == 5
58
+
59
+
60
+ def test_serialize_value_complex_object():
61
+ """Test _serialize_value with custom objects."""
62
+
63
+ @dataclass
64
+ class CustomObj:
65
+ name: str
66
+ value: int
67
+
68
+ obj = CustomObj(name="test", value=42)
69
+ result = _serialize_value(obj)
70
+ assert isinstance(result, dict)
71
+ assert result["name"] == "test"
72
+ assert result["value"] == 42
73
+
74
+
75
+ def test_serialize_value_fallback():
76
+ """Test _serialize_value fallback for non-serializable objects."""
77
+
78
+ class WeirdObj:
79
+ def __init__(self):
80
+ raise Exception("Can't access")
81
+
82
+ obj = WeirdObj.__new__(WeirdObj)
83
+ result = _serialize_value(obj)
84
+ # The result is a string representation of the object
85
+ assert isinstance(result, str)
86
+ assert "WeirdObj" in result
87
+
88
+
89
+ @pytest.mark.asyncio
90
+ async def test_instrument_async_basic():
91
+ """Test instrument decorator on async function."""
92
+
93
+ @instrument
94
+ async def test_func(x: int, y: int) -> int:
95
+ return x + y
96
+
97
+ result = await test_func(2, 3)
98
+ assert result == 5
99
+
100
+
101
+ @pytest.mark.asyncio
102
+ async def test_instrument_async_with_params():
103
+ """Test instrument with custom parameters."""
104
+
105
+ @instrument(name="custom_name", span_type="custom_type")
106
+ async def test_func(x: int) -> int:
107
+ return x * 2
108
+
109
+ result = await test_func(5)
110
+ assert result == 10
111
+
112
+
113
+ @pytest.mark.asyncio
114
+ async def test_instrument_async_with_exception():
115
+ """Test instrument handles exceptions."""
116
+
117
+ @instrument
118
+ async def test_func():
119
+ raise ValueError("Test error")
120
+
121
+ with pytest.raises(ValueError, match="Test error"):
122
+ await test_func()
123
+
124
+
125
+ @pytest.mark.asyncio
126
+ async def test_instrument_async_no_record_args():
127
+ """Test instrument with record_args=False."""
128
+
129
+ @instrument(record_args=False)
130
+ async def test_func(x: int) -> int:
131
+ return x
132
+
133
+ result = await test_func(42)
134
+ assert result == 42
135
+
136
+
137
+ @pytest.mark.asyncio
138
+ async def test_instrument_async_no_record_result():
139
+ """Test instrument with record_result=False."""
140
+
141
+ @instrument(record_result=False)
142
+ async def test_func() -> str:
143
+ return "test"
144
+
145
+ result = await test_func()
146
+ assert result == "test"
147
+
148
+
149
+ @pytest.mark.asyncio
150
+ async def test_instrument_async_with_attributes():
151
+ """Test instrument with custom attributes."""
152
+
153
+ @instrument(attributes={"custom_attr": "value"})
154
+ async def test_func() -> int:
155
+ return 42
156
+
157
+ result = await test_func()
158
+ assert result == 42
159
+
160
+
161
+ @pytest.mark.asyncio
162
+ async def test_instrument_async_with_span_kind():
163
+ """Test instrument with custom span kind."""
164
+
165
+ @instrument(span_kind=SpanKind.CLIENT)
166
+ async def test_func() -> int:
167
+ return 1
168
+
169
+ result = await test_func()
170
+ assert result == 1
171
+
172
+
173
+ def test_instrument_sync_basic():
174
+ """Test instrument decorator on sync function."""
175
+
176
+ @instrument
177
+ def test_func(x: int, y: int) -> int:
178
+ return x + y
179
+
180
+ result = test_func(2, 3)
181
+ assert result == 5
182
+
183
+
184
+ def test_instrument_sync_with_params():
185
+ """Test instrument on sync function with parameters."""
186
+
187
+ @instrument(name="sync_custom", span_type="sync_type")
188
+ def test_func(x: int) -> int:
189
+ return x * 2
190
+
191
+ result = test_func(5)
192
+ assert result == 10
193
+
194
+
195
+ def test_instrument_sync_with_exception():
196
+ """Test instrument handles exceptions in sync functions."""
197
+
198
+ @instrument
199
+ def test_func():
200
+ raise ValueError("Sync error")
201
+
202
+ with pytest.raises(ValueError, match="Sync error"):
203
+ test_func()
204
+
205
+
206
+ def test_instrument_sync_no_record_args():
207
+ """Test instrument sync with record_args=False."""
208
+
209
+ @instrument(record_args=False)
210
+ def test_func(x: int) -> int:
211
+ return x
212
+
213
+ result = test_func(42)
214
+ assert result == 42
215
+
216
+
217
+ def test_instrument_sync_no_record_result():
218
+ """Test instrument sync with record_result=False."""
219
+
220
+ @instrument(record_result=False)
221
+ def test_func() -> str:
222
+ return "test"
223
+
224
+ result = test_func()
225
+ assert result == "test"
226
+
227
+
228
+ def test_instrument_sync_with_attributes():
229
+ """Test instrument sync with custom attributes."""
230
+
231
+ @instrument(attributes={"sync_attr": "sync_value"})
232
+ def test_func() -> int:
233
+ return 42
234
+
235
+ result = test_func()
236
+ assert result == 42
237
+
238
+
239
+ def test_instrument_already_instrumented():
240
+ """Test that instrumenting already instrumented function is skipped."""
241
+
242
+ @instrument
243
+ def test_func():
244
+ return "original"
245
+
246
+ # Try to instrument again
247
+ test_func2 = instrument(test_func)
248
+
249
+ # Should be the same function
250
+ assert test_func2 is test_func
251
+
252
+
253
+ def test_instrument_marks_as_instrumented():
254
+ """Test that instrument marks functions correctly."""
255
+
256
+ @instrument
257
+ def test_func():
258
+ return True
259
+
260
+ assert hasattr(test_func, "_hud_instrumented")
261
+ assert test_func._hud_instrumented is True
262
+ assert hasattr(test_func, "_hud_original")
263
+
264
+
265
+ @pytest.mark.asyncio
266
+ async def test_instrument_async_complex_result():
267
+ """Test instrument with complex result object."""
268
+
269
+ @instrument
270
+ async def test_func() -> dict:
271
+ return {"nested": {"data": [1, 2, 3]}, "count": 3}
272
+
273
+ result = await test_func()
274
+ assert result["count"] == 3
275
+
276
+
277
+ def test_instrument_sync_complex_result():
278
+ """Test instrument sync with complex result."""
279
+
280
+ @dataclass
281
+ class Result:
282
+ value: int
283
+ name: str
284
+
285
+ @instrument
286
+ def test_func() -> Result:
287
+ return Result(value=42, name="test")
288
+
289
+ result = test_func()
290
+ assert result.value == 42
291
+
292
+
293
+ @pytest.mark.asyncio
294
+ async def test_instrument_async_with_self_param():
295
+ """Test instrument properly handles 'self' parameter."""
296
+
297
+ class TestClass:
298
+ @instrument
299
+ async def method(self, x: int) -> int:
300
+ return x * 2
301
+
302
+ obj = TestClass()
303
+ result = await obj.method(5)
304
+ assert result == 10
305
+
306
+
307
+ def test_instrument_sync_with_cls_param():
308
+ """Test instrument properly handles 'cls' parameter."""
309
+
310
+ class TestClass:
311
+ @classmethod
312
+ @instrument
313
+ def method(cls, x: int) -> int:
314
+ return x * 3
315
+
316
+ result = TestClass.method(4)
317
+ assert result == 12
318
+
319
+
320
+ @pytest.mark.asyncio
321
+ async def test_instrument_async_serialization_error():
322
+ """Test instrument handles serialization errors gracefully."""
323
+
324
+ class UnserializableArg:
325
+ def __getattribute__(self, name):
326
+ raise Exception("Can't serialize")
327
+
328
+ @instrument
329
+ async def test_func(arg):
330
+ return "success"
331
+
332
+ # Should not raise, just skip serialization
333
+ result = await test_func(UnserializableArg())
334
+ assert result == "success"
335
+
336
+
337
+ def test_instrument_function_without_signature():
338
+ """Test instrument on functions without inspectable signature."""
339
+ # Built-in functions don't have signatures
340
+ instrumented_len = instrument(len)
341
+ result = instrumented_len([1, 2, 3])
342
+ assert result == 3
343
+
344
+
345
+ @pytest.mark.asyncio
346
+ async def test_instrument_async_result_serialization_error():
347
+ """Test instrument handles result serialization errors."""
348
+
349
+ class UnserializableResult:
350
+ def __iter__(self):
351
+ raise Exception("Can't iterate")
352
+
353
+ @instrument
354
+ async def test_func():
355
+ return UnserializableResult()
356
+
357
+ # Should not raise, just skip result recording
358
+ result = await test_func()
359
+ assert isinstance(result, UnserializableResult)
360
+
361
+
362
+ def test_instrument_without_parentheses():
363
+ """Test using @instrument without parentheses."""
364
+
365
+ @instrument
366
+ def test_func(x: int) -> int:
367
+ return x + 1
368
+
369
+ assert test_func(5) == 6
370
+
371
+
372
+ def test_instrument_with_parentheses():
373
+ """Test using @instrument() with parentheses."""
374
+
375
+ @instrument()
376
+ def test_func(x: int) -> int:
377
+ return x + 1
378
+
379
+ assert test_func(5) == 6
380
+
381
+
382
+ @pytest.mark.asyncio
383
+ async def test_instrument_async_with_defaults():
384
+ """Test instrument with function that has default arguments."""
385
+
386
+ @instrument
387
+ async def test_func(x: int, y: int = 10) -> int:
388
+ return x + y
389
+
390
+ assert await test_func(5) == 15
391
+ assert await test_func(5, 20) == 25
392
+
393
+
394
+ def test_instrument_sync_with_kwargs():
395
+ """Test instrument with keyword arguments."""
396
+
397
+ @instrument
398
+ def test_func(x: int, **kwargs) -> dict:
399
+ return {"x": x, **kwargs}
400
+
401
+ result = test_func(1, a=2, b=3)
402
+ assert result == {"x": 1, "a": 2, "b": 3}
403
+
404
+
405
+ @pytest.mark.asyncio
406
+ async def test_instrument_async_with_varargs():
407
+ """Test instrument with *args."""
408
+
409
+ @instrument
410
+ async def test_func(*args) -> int:
411
+ return sum(args)
412
+
413
+ result = await test_func(1, 2, 3, 4)
414
+ assert result == 10