hud-python 0.3.4__py3-none-any.whl → 0.4.0__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 (192) hide show
  1. hud/__init__.py +22 -89
  2. hud/agents/__init__.py +17 -0
  3. hud/agents/art.py +101 -0
  4. hud/agents/base.py +599 -0
  5. hud/{mcp → agents}/claude.py +373 -321
  6. hud/{mcp → agents}/langchain.py +250 -250
  7. hud/agents/misc/__init__.py +7 -0
  8. hud/{agent → agents}/misc/response_agent.py +80 -80
  9. hud/{mcp → agents}/openai.py +352 -334
  10. hud/agents/openai_chat_generic.py +154 -0
  11. hud/{mcp → agents}/tests/__init__.py +1 -1
  12. hud/agents/tests/test_base.py +742 -0
  13. hud/agents/tests/test_claude.py +324 -0
  14. hud/{mcp → agents}/tests/test_client.py +363 -324
  15. hud/{mcp → agents}/tests/test_openai.py +237 -238
  16. hud/cli/__init__.py +617 -0
  17. hud/cli/__main__.py +8 -0
  18. hud/cli/analyze.py +371 -0
  19. hud/cli/analyze_metadata.py +230 -0
  20. hud/cli/build.py +427 -0
  21. hud/cli/clone.py +185 -0
  22. hud/cli/cursor.py +92 -0
  23. hud/cli/debug.py +392 -0
  24. hud/cli/docker_utils.py +83 -0
  25. hud/cli/init.py +281 -0
  26. hud/cli/interactive.py +353 -0
  27. hud/cli/mcp_server.py +756 -0
  28. hud/cli/pull.py +336 -0
  29. hud/cli/push.py +379 -0
  30. hud/cli/remote_runner.py +311 -0
  31. hud/cli/runner.py +160 -0
  32. hud/cli/tests/__init__.py +3 -0
  33. hud/cli/tests/test_analyze.py +284 -0
  34. hud/cli/tests/test_cli_init.py +265 -0
  35. hud/cli/tests/test_cli_main.py +27 -0
  36. hud/cli/tests/test_clone.py +142 -0
  37. hud/cli/tests/test_cursor.py +253 -0
  38. hud/cli/tests/test_debug.py +453 -0
  39. hud/cli/tests/test_mcp_server.py +139 -0
  40. hud/cli/tests/test_utils.py +388 -0
  41. hud/cli/utils.py +263 -0
  42. hud/clients/README.md +143 -0
  43. hud/clients/__init__.py +16 -0
  44. hud/clients/base.py +354 -0
  45. hud/clients/fastmcp.py +202 -0
  46. hud/clients/mcp_use.py +278 -0
  47. hud/clients/tests/__init__.py +1 -0
  48. hud/clients/tests/test_client_integration.py +111 -0
  49. hud/clients/tests/test_fastmcp.py +342 -0
  50. hud/clients/tests/test_protocol.py +188 -0
  51. hud/clients/utils/__init__.py +1 -0
  52. hud/clients/utils/retry_transport.py +160 -0
  53. hud/datasets.py +322 -192
  54. hud/misc/__init__.py +1 -0
  55. hud/{agent → misc}/claude_plays_pokemon.py +292 -283
  56. hud/otel/__init__.py +35 -0
  57. hud/otel/collector.py +142 -0
  58. hud/otel/config.py +164 -0
  59. hud/otel/context.py +536 -0
  60. hud/otel/exporters.py +366 -0
  61. hud/otel/instrumentation.py +97 -0
  62. hud/otel/processors.py +118 -0
  63. hud/otel/tests/__init__.py +1 -0
  64. hud/otel/tests/test_processors.py +197 -0
  65. hud/server/__init__.py +5 -5
  66. hud/server/context.py +114 -0
  67. hud/server/helper/__init__.py +5 -0
  68. hud/server/low_level.py +132 -0
  69. hud/server/server.py +166 -0
  70. hud/server/tests/__init__.py +3 -0
  71. hud/settings.py +73 -79
  72. hud/shared/__init__.py +5 -0
  73. hud/{exceptions.py → shared/exceptions.py} +180 -180
  74. hud/{server → shared}/requests.py +264 -264
  75. hud/shared/tests/test_exceptions.py +157 -0
  76. hud/{server → shared}/tests/test_requests.py +275 -275
  77. hud/telemetry/__init__.py +25 -30
  78. hud/telemetry/instrument.py +379 -0
  79. hud/telemetry/job.py +309 -141
  80. hud/telemetry/replay.py +74 -0
  81. hud/telemetry/trace.py +83 -0
  82. hud/tools/__init__.py +33 -34
  83. hud/tools/base.py +365 -65
  84. hud/tools/bash.py +161 -137
  85. hud/tools/computer/__init__.py +15 -13
  86. hud/tools/computer/anthropic.py +437 -414
  87. hud/tools/computer/hud.py +376 -328
  88. hud/tools/computer/openai.py +295 -286
  89. hud/tools/computer/settings.py +82 -0
  90. hud/tools/edit.py +314 -290
  91. hud/tools/executors/__init__.py +30 -30
  92. hud/tools/executors/base.py +539 -532
  93. hud/tools/executors/pyautogui.py +621 -619
  94. hud/tools/executors/tests/__init__.py +1 -1
  95. hud/tools/executors/tests/test_base_executor.py +338 -338
  96. hud/tools/executors/tests/test_pyautogui_executor.py +165 -165
  97. hud/tools/executors/xdo.py +511 -503
  98. hud/tools/{playwright_tool.py → playwright.py} +412 -379
  99. hud/tools/tests/__init__.py +3 -3
  100. hud/tools/tests/test_base.py +282 -0
  101. hud/tools/tests/test_bash.py +158 -152
  102. hud/tools/tests/test_bash_extended.py +197 -0
  103. hud/tools/tests/test_computer.py +425 -52
  104. hud/tools/tests/test_computer_actions.py +34 -34
  105. hud/tools/tests/test_edit.py +259 -240
  106. hud/tools/tests/test_init.py +27 -27
  107. hud/tools/tests/test_playwright_tool.py +183 -183
  108. hud/tools/tests/test_tools.py +145 -157
  109. hud/tools/tests/test_utils.py +156 -156
  110. hud/tools/types.py +72 -0
  111. hud/tools/utils.py +50 -50
  112. hud/types.py +136 -89
  113. hud/utils/__init__.py +10 -16
  114. hud/utils/async_utils.py +65 -0
  115. hud/utils/design.py +168 -0
  116. hud/utils/mcp.py +55 -0
  117. hud/utils/progress.py +149 -149
  118. hud/utils/telemetry.py +66 -66
  119. hud/utils/tests/test_async_utils.py +173 -0
  120. hud/utils/tests/test_init.py +17 -21
  121. hud/utils/tests/test_progress.py +261 -225
  122. hud/utils/tests/test_telemetry.py +82 -37
  123. hud/utils/tests/test_version.py +8 -8
  124. hud/version.py +7 -7
  125. hud_python-0.4.0.dist-info/METADATA +474 -0
  126. hud_python-0.4.0.dist-info/RECORD +132 -0
  127. hud_python-0.4.0.dist-info/entry_points.txt +3 -0
  128. {hud_python-0.3.4.dist-info → hud_python-0.4.0.dist-info}/licenses/LICENSE +21 -21
  129. hud/adapters/__init__.py +0 -8
  130. hud/adapters/claude/__init__.py +0 -5
  131. hud/adapters/claude/adapter.py +0 -180
  132. hud/adapters/claude/tests/__init__.py +0 -1
  133. hud/adapters/claude/tests/test_adapter.py +0 -519
  134. hud/adapters/common/__init__.py +0 -6
  135. hud/adapters/common/adapter.py +0 -178
  136. hud/adapters/common/tests/test_adapter.py +0 -289
  137. hud/adapters/common/types.py +0 -446
  138. hud/adapters/operator/__init__.py +0 -5
  139. hud/adapters/operator/adapter.py +0 -108
  140. hud/adapters/operator/tests/__init__.py +0 -1
  141. hud/adapters/operator/tests/test_adapter.py +0 -370
  142. hud/agent/__init__.py +0 -19
  143. hud/agent/base.py +0 -126
  144. hud/agent/claude.py +0 -271
  145. hud/agent/langchain.py +0 -215
  146. hud/agent/misc/__init__.py +0 -3
  147. hud/agent/operator.py +0 -268
  148. hud/agent/tests/__init__.py +0 -1
  149. hud/agent/tests/test_base.py +0 -202
  150. hud/env/__init__.py +0 -11
  151. hud/env/client.py +0 -35
  152. hud/env/docker_client.py +0 -349
  153. hud/env/environment.py +0 -446
  154. hud/env/local_docker_client.py +0 -358
  155. hud/env/remote_client.py +0 -212
  156. hud/env/remote_docker_client.py +0 -292
  157. hud/gym.py +0 -130
  158. hud/job.py +0 -773
  159. hud/mcp/__init__.py +0 -17
  160. hud/mcp/base.py +0 -631
  161. hud/mcp/client.py +0 -312
  162. hud/mcp/tests/test_base.py +0 -512
  163. hud/mcp/tests/test_claude.py +0 -294
  164. hud/task.py +0 -149
  165. hud/taskset.py +0 -237
  166. hud/telemetry/_trace.py +0 -347
  167. hud/telemetry/context.py +0 -230
  168. hud/telemetry/exporter.py +0 -575
  169. hud/telemetry/instrumentation/__init__.py +0 -3
  170. hud/telemetry/instrumentation/mcp.py +0 -259
  171. hud/telemetry/instrumentation/registry.py +0 -59
  172. hud/telemetry/mcp_models.py +0 -270
  173. hud/telemetry/tests/__init__.py +0 -1
  174. hud/telemetry/tests/test_context.py +0 -210
  175. hud/telemetry/tests/test_trace.py +0 -312
  176. hud/tools/helper/README.md +0 -56
  177. hud/tools/helper/__init__.py +0 -9
  178. hud/tools/helper/mcp_server.py +0 -78
  179. hud/tools/helper/server_initialization.py +0 -115
  180. hud/tools/helper/utils.py +0 -58
  181. hud/trajectory.py +0 -94
  182. hud/utils/agent.py +0 -37
  183. hud/utils/common.py +0 -256
  184. hud/utils/config.py +0 -120
  185. hud/utils/deprecation.py +0 -115
  186. hud/utils/misc.py +0 -53
  187. hud/utils/tests/test_common.py +0 -277
  188. hud/utils/tests/test_config.py +0 -129
  189. hud_python-0.3.4.dist-info/METADATA +0 -284
  190. hud_python-0.3.4.dist-info/RECORD +0 -120
  191. /hud/{adapters/common → shared}/tests/__init__.py +0 -0
  192. {hud_python-0.3.4.dist-info → hud_python-0.4.0.dist-info}/WHEEL +0 -0
hud/utils/deprecation.py DELETED
@@ -1,115 +0,0 @@
1
- """Deprecation utilities for HUD SDK."""
2
-
3
- from __future__ import annotations
4
-
5
- import functools
6
- import logging
7
- import warnings
8
- from typing import TYPE_CHECKING, Any, TypeVar, cast
9
-
10
- logger = logging.getLogger(__name__)
11
-
12
- if TYPE_CHECKING:
13
- from collections.abc import Callable
14
- T = TypeVar("T")
15
-
16
-
17
- def deprecated(
18
- reason: str,
19
- *,
20
- version: str | None = None,
21
- replacement: str | None = None,
22
- removal_version: str | None = None,
23
- ) -> Callable[[T], T]:
24
- """
25
- Decorator to mark functions, methods, or classes as deprecated.
26
-
27
- Args:
28
- reason: Explanation of why this is deprecated
29
- version: Version when this was deprecated (e.g., "1.0.0")
30
- replacement: What to use instead
31
- removal_version: Version when this will be removed
32
-
33
- Example:
34
- @deprecated(
35
- reason="Use TaskConfig instead",
36
- replacement="hud.datasets.TaskConfig",
37
- version="0.3.0",
38
- removal_version="0.4.0"
39
- )
40
- class OldClass:
41
- pass
42
- """
43
-
44
- def decorator(obj: T) -> T:
45
- message_parts = [f"{obj.__module__}.{obj.__qualname__} is deprecated"]
46
-
47
- if version:
48
- message_parts.append(f"(deprecated since v{version})")
49
-
50
- message_parts.append(f": {reason}")
51
-
52
- if replacement:
53
- message_parts.append(f". Use {replacement} instead")
54
-
55
- if removal_version:
56
- message_parts.append(f". Will be removed in v{removal_version}")
57
-
58
- deprecation_message = " ".join(message_parts) + "."
59
-
60
- if isinstance(obj, type):
61
- # Handle class deprecation
62
- original_init = obj.__init__
63
-
64
- @functools.wraps(original_init)
65
- def new_init(self: Any, *args: Any, **kwargs: Any) -> None:
66
- warnings.warn(deprecation_message, DeprecationWarning, stacklevel=2)
67
- logger.warning(deprecation_message)
68
- original_init(self, *args, **kwargs)
69
-
70
- obj.__init__ = new_init
71
-
72
- # Update docstring
73
- if obj.__doc__:
74
- obj.__doc__ = f"**DEPRECATED**: {deprecation_message}\n\n{obj.__doc__}"
75
- else:
76
- obj.__doc__ = f"**DEPRECATED**: {deprecation_message}"
77
-
78
- else:
79
- # Handle function/method deprecation
80
- func = cast("Callable[..., Any]", obj)
81
-
82
- @functools.wraps(func)
83
- def wrapper(*args: Any, **kwargs: Any) -> Any:
84
- warnings.warn(deprecation_message, DeprecationWarning, stacklevel=2)
85
- logger.warning(deprecation_message)
86
- return func(*args, **kwargs)
87
-
88
- # Update docstring
89
- if wrapper.__doc__:
90
- wrapper.__doc__ = f"**DEPRECATED**: {deprecation_message}\n\n{wrapper.__doc__}"
91
- else:
92
- wrapper.__doc__ = f"**DEPRECATED**: {deprecation_message}"
93
-
94
- return cast("T", wrapper)
95
-
96
- return obj
97
-
98
- return decorator
99
-
100
-
101
- def emit_deprecation_warning(
102
- message: str,
103
- category: type[Warning] = DeprecationWarning,
104
- stacklevel: int = 2,
105
- ) -> None:
106
- """
107
- Emit a deprecation warning with both warnings and logging.
108
-
109
- Args:
110
- message: The deprecation message
111
- category: Warning category (default: DeprecationWarning)
112
- stacklevel: Stack level for warning (default: 2)
113
- """
114
- warnings.warn(message, category, stacklevel=stacklevel)
115
- logger.warning(message)
hud/utils/misc.py DELETED
@@ -1,53 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import logging
4
- from typing import TYPE_CHECKING, Any
5
-
6
- from hud.server import make_request
7
- from hud.settings import settings
8
-
9
- if TYPE_CHECKING:
10
- from hud.env.environment import Environment # Import Environment for type hinting
11
-
12
- logger = logging.getLogger(__name__)
13
-
14
-
15
- async def upload_env_telemetry(
16
- environment: Environment,
17
- results: Any,
18
- api_key: str | None = None,
19
- ) -> None:
20
- """
21
- Sends telemetry data (results from a cloud runner) to the HUD telemetry upload endpoint.
22
- """
23
- environment_id = environment.client.env_id # type: ignore
24
-
25
- if not api_key:
26
- api_key = settings.api_key
27
-
28
- if not api_key:
29
- raise ValueError("API key must be provided either as an argument or set in hud.settings.")
30
-
31
- endpoint_url = f"{settings.base_url}/v2/environments/{environment_id}/telemetry-upload"
32
-
33
- request_payload = {
34
- "results": {
35
- "steps": results,
36
- }
37
- }
38
-
39
- logger.debug("Sending telemetry to %s for env_id: %s", endpoint_url, environment_id)
40
-
41
- try:
42
- await make_request(
43
- method="POST",
44
- url=endpoint_url,
45
- json=request_payload,
46
- api_key=api_key,
47
- )
48
- logger.info("Successfully uploaded telemetry for environment_id: %s", environment_id)
49
- except Exception as e:
50
- logger.error(
51
- "Failed to upload telemetry for environment_id: %s. Error: %s", environment_id, e
52
- )
53
- raise
@@ -1,277 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import io
4
- import tarfile
5
- from pathlib import Path
6
- from typing import TYPE_CHECKING
7
-
8
- import pytest
9
-
10
- from hud.utils.common import directory_to_tar_bytes, get_gym_id
11
-
12
- if TYPE_CHECKING:
13
- import pytest_mock
14
-
15
-
16
- def test_directory_to_tar_bytes(tmpdir_factory: pytest.TempdirFactory):
17
- """Test that a directory can be converted to a tar bytes object."""
18
- temp_dir = tmpdir_factory.mktemp("test_dir")
19
- temp_dir_path = Path(temp_dir)
20
-
21
- (temp_dir_path / "test.txt").write_text("test content")
22
-
23
- nested_dir = temp_dir_path / "nested"
24
- nested_dir.mkdir(exist_ok=True)
25
- (nested_dir / "file.txt").write_text("nested content")
26
-
27
- tar_bytes = directory_to_tar_bytes(temp_dir_path)
28
- assert tar_bytes is not None
29
- assert len(tar_bytes) > 0
30
-
31
- with tarfile.open(fileobj=io.BytesIO(tar_bytes), mode="r:*") as tar:
32
- members = tar.getmembers()
33
- member_names = {m.name for m in members}
34
-
35
- assert "test.txt" in member_names
36
- assert "nested/file.txt" in member_names
37
-
38
- test_content = tar.extractfile("test.txt")
39
- assert test_content is not None
40
- assert test_content.read().decode() == "test content"
41
-
42
- nested_content = tar.extractfile("nested/file.txt")
43
- assert nested_content is not None
44
- assert nested_content.read().decode() == "nested content"
45
-
46
-
47
- @pytest.mark.asyncio
48
- async def test_get_gym_id(mocker: pytest_mock.MockerFixture):
49
- """Test that the gym ID can be retrieved."""
50
- mocker.patch("hud.utils.common.make_request", return_value={"id": "test_gym_id"})
51
- gym_id = await get_gym_id("test_gym")
52
- assert gym_id == "test_gym_id"
53
-
54
-
55
- def test_function_config_stores_function_name_args_and_optional_id():
56
- """FunctionConfig should store function name, args list, and optional id."""
57
- from hud.utils.common import FunctionConfig
58
-
59
- # Minimal config
60
- minimal = FunctionConfig(function="test_func", args=[])
61
- assert minimal.function == "test_func"
62
- assert minimal.args == []
63
- assert minimal.id is None
64
-
65
- # With args
66
- with_args = FunctionConfig(function="navigate", args=["https://example.com", {"wait": True}])
67
- assert with_args.function == "navigate"
68
- assert len(with_args.args) == 2
69
- assert with_args.args[0] == "https://example.com"
70
- assert with_args.args[1] == {"wait": True}
71
-
72
- # With id
73
- with_id = FunctionConfig(
74
- function="complex_operation",
75
- args=[42, "test", {"nested": {"key": "value"}}],
76
- id="op_123",
77
- )
78
- assert with_id.function == "complex_operation"
79
- assert len(with_id.args) == 3
80
- assert with_id.id == "op_123"
81
-
82
-
83
- @pytest.mark.asyncio
84
- async def test_get_gym_id_fetches_id_from_api_response(
85
- mocker: pytest_mock.MockerFixture,
86
- ):
87
- """get_gym_id should extract 'id' field from API response."""
88
- # Arrange
89
- api_response = {"id": "gym-123", "name": "Test Gym", "status": "active"}
90
- mocker.patch("hud.utils.common.make_request", return_value=api_response)
91
-
92
- # Act
93
- gym_id = await get_gym_id("test_gym")
94
-
95
- # Assert
96
- assert gym_id == "gym-123"
97
-
98
-
99
- @pytest.mark.asyncio
100
- async def test_get_gym_id_propagates_network_errors(mocker: pytest_mock.MockerFixture):
101
- """get_gym_id should propagate exceptions from make_request."""
102
- # Arrange
103
- mocker.patch("hud.utils.common.make_request", side_effect=ConnectionError("API unavailable"))
104
-
105
- # Act & Assert
106
- with pytest.raises(ConnectionError, match="API unavailable"):
107
- await get_gym_id("test_gym")
108
-
109
-
110
- @pytest.mark.asyncio
111
- async def test_get_gym_id_raises_key_error_when_id_missing(
112
- mocker: pytest_mock.MockerFixture,
113
- ):
114
- """get_gym_id should raise KeyError when response lacks 'id' field."""
115
- # Arrange
116
- incomplete_response = {"name": "Test Gym", "status": "active"} # Missing 'id'
117
- mocker.patch("hud.utils.common.make_request", return_value=incomplete_response)
118
-
119
- # Act & Assert
120
- with pytest.raises(KeyError):
121
- await get_gym_id("test_gym")
122
-
123
-
124
- def test_directory_to_tar_bytes_creates_valid_tar_archive(
125
- tmpdir_factory: pytest.TempdirFactory,
126
- ):
127
- """directory_to_tar_bytes should create a valid tar archive containing all files."""
128
- # Arrange
129
- temp_dir = tmpdir_factory.mktemp("test_archive")
130
- temp_dir_path = Path(temp_dir)
131
-
132
- # Create test structure
133
- (temp_dir_path / "file1.txt").write_text("content1")
134
- (temp_dir_path / "file2.py").write_text("import os\nprint('hello')")
135
-
136
- subdir = temp_dir_path / "subdir"
137
- subdir.mkdir()
138
- (subdir / "nested.json").write_text('{"key": "value"}')
139
-
140
- # Act
141
- tar_bytes = directory_to_tar_bytes(temp_dir_path)
142
-
143
- # Assert
144
- assert isinstance(tar_bytes, bytes)
145
- assert len(tar_bytes) > 0
146
-
147
- # Verify contents
148
- with tarfile.open(fileobj=io.BytesIO(tar_bytes), mode="r:*") as tar:
149
- members = {m.name for m in tar.getmembers()}
150
- assert "file1.txt" in members
151
- assert "file2.py" in members
152
- assert "subdir/nested.json" in members
153
-
154
- # Verify file contents
155
- content = tar.extractfile("file1.txt")
156
- assert content is not None
157
- assert content.read().decode() == "content1"
158
-
159
-
160
- def test_directory_to_tar_bytes_handles_empty_directory(
161
- tmpdir_factory: pytest.TempdirFactory,
162
- ):
163
- """directory_to_tar_bytes should handle empty directories gracefully."""
164
- # Arrange
165
- empty_dir = tmpdir_factory.mktemp("empty")
166
- empty_dir_path = Path(empty_dir)
167
-
168
- # Act
169
- tar_bytes = directory_to_tar_bytes(empty_dir_path)
170
-
171
- # Assert
172
- assert isinstance(tar_bytes, bytes)
173
- assert len(tar_bytes) > 0 # Even empty tar has headers
174
-
175
- with tarfile.open(fileobj=io.BytesIO(tar_bytes), mode="r:*") as tar:
176
- members = tar.getmembers()
177
- # May contain the directory itself or be completely empty
178
- assert len(members) >= 0
179
-
180
-
181
- def test_directory_to_tar_bytes_preserves_directory_structure(
182
- tmpdir_factory: pytest.TempdirFactory,
183
- ):
184
- """directory_to_tar_bytes should preserve nested directory structure."""
185
- # Arrange
186
- root = tmpdir_factory.mktemp("root")
187
- root_path = Path(root)
188
-
189
- # Create nested structure
190
- (root_path / "a" / "b" / "c").mkdir(parents=True)
191
- (root_path / "a" / "file1.txt").write_text("in a")
192
- (root_path / "a" / "b" / "file2.txt").write_text("in b")
193
- (root_path / "a" / "b" / "c" / "file3.txt").write_text("in c")
194
-
195
- # Act
196
- tar_bytes = directory_to_tar_bytes(root_path)
197
-
198
- # Assert
199
- with tarfile.open(fileobj=io.BytesIO(tar_bytes), mode="r:*") as tar:
200
- members = {m.name for m in tar.getmembers()}
201
- assert "a/file1.txt" in members
202
- assert "a/b/file2.txt" in members
203
- assert "a/b/c/file3.txt" in members
204
-
205
-
206
- def test_directory_to_tar_bytes_with_exclusions(tmpdir_factory: pytest.TempdirFactory):
207
- """Test directory_to_tar_bytes with files to exclude."""
208
- temp_dir = tmpdir_factory.mktemp("test_exclude_dir")
209
- temp_dir_path = Path(temp_dir)
210
-
211
- # Create various files
212
- (temp_dir_path / "include_me.txt").write_text("include")
213
- (temp_dir_path / ".git").mkdir()
214
- (temp_dir_path / ".git" / "config").write_text("git config")
215
- (temp_dir_path / "__pycache__").mkdir()
216
- (temp_dir_path / "__pycache__" / "module.pyc").write_bytes(b"pyc content")
217
- (temp_dir_path / "normal_dir").mkdir()
218
- (temp_dir_path / "normal_dir" / "file.py").write_text("python code")
219
-
220
- tar_bytes = directory_to_tar_bytes(temp_dir_path)
221
-
222
- # Check contents
223
- with tarfile.open(fileobj=io.BytesIO(tar_bytes), mode="r:*") as tar:
224
- member_names = {m.name for m in tar.getmembers()}
225
-
226
- # Should include regular files and directories
227
- assert "include_me.txt" in member_names
228
- assert "normal_dir/file.py" in member_names
229
-
230
- # Implementation might exclude common patterns like .git and __pycache__
231
- # This depends on the actual implementation
232
-
233
-
234
- def test_directory_to_tar_bytes_empty_directory(tmpdir_factory: pytest.TempdirFactory):
235
- """Test directory_to_tar_bytes with empty directory."""
236
- temp_dir = tmpdir_factory.mktemp("empty_dir")
237
- temp_dir_path = Path(temp_dir)
238
-
239
- tar_bytes = directory_to_tar_bytes(temp_dir_path)
240
-
241
- # Should still create a valid tar even if empty
242
- assert tar_bytes is not None
243
- assert len(tar_bytes) > 0
244
-
245
- with tarfile.open(fileobj=io.BytesIO(tar_bytes), mode="r:*") as tar:
246
- members = tar.getmembers()
247
- # Might be empty or contain just the root directory
248
- assert len(members) >= 0
249
-
250
-
251
- def test_directory_to_tar_bytes_symlinks(tmpdir_factory: pytest.TempdirFactory):
252
- """Test directory_to_tar_bytes with symbolic links."""
253
- temp_dir = tmpdir_factory.mktemp("symlink_dir")
254
- temp_dir_path = Path(temp_dir)
255
-
256
- # Create a file and a symlink to it
257
- target_file = temp_dir_path / "target.txt"
258
- target_file.write_text("target content")
259
-
260
- symlink = temp_dir_path / "link_to_target.txt"
261
- try:
262
- symlink.symlink_to(target_file)
263
- has_symlink = True
264
- except OSError:
265
- # Symlinks might not be supported on all systems (e.g., Windows without admin)
266
- has_symlink = False
267
-
268
- tar_bytes = directory_to_tar_bytes(temp_dir_path)
269
-
270
- with tarfile.open(fileobj=io.BytesIO(tar_bytes), mode="r:*") as tar:
271
- members = {m.name: m for m in tar.getmembers()}
272
-
273
- assert "target.txt" in members
274
-
275
- if has_symlink:
276
- # Check how symlinks are handled (might be followed or preserved)
277
- assert "link_to_target.txt" in members
@@ -1,129 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import pytest
4
-
5
- from hud.utils.common import FunctionConfig
6
- from hud.utils.config import (
7
- _is_list_of_configs,
8
- _is_valid_python_name,
9
- _split_and_validate_path,
10
- _validate_hud_config,
11
- expand_config,
12
- )
13
-
14
-
15
- @pytest.mark.parametrize(
16
- "config, expected",
17
- [
18
- ("test", [{"function": "test", "args": [], "id": None}]),
19
- (("test",), [{"function": "test", "args": [], "id": None}]),
20
- (
21
- [FunctionConfig(function="test", args=[])],
22
- [{"function": "test", "args": [], "id": None}],
23
- ),
24
- ({"function": "test", "args": []}, [{"function": "test", "args": [], "id": None}]),
25
- (
26
- {"function": "test", "args": ["arg1"]},
27
- [{"function": "test", "args": ["arg1"], "id": None}],
28
- ),
29
- (
30
- {"function": "test", "args": ["arg1"], "id": "test_id"},
31
- [{"function": "test", "args": ["arg1"], "id": "test_id"}],
32
- ),
33
- (("test", "arg1", "arg2"), [{"function": "test", "args": ["arg1", "arg2"], "id": None}]),
34
- ],
35
- )
36
- def test_expand_config(config, expected):
37
- result = expand_config(config)
38
- assert len(result) == len(expected)
39
- for i, item in enumerate(result):
40
- assert item.function == expected[i]["function"]
41
- assert item.args == expected[i]["args"]
42
- assert item.id == expected[i]["id"]
43
-
44
-
45
- @pytest.mark.parametrize(
46
- "name, expected",
47
- [
48
- ("valid_name", True),
49
- ("ValidName", True),
50
- ("valid_name_123", True),
51
- ("_valid_name", True),
52
- ("123_invalid", False),
53
- ("invalid-name", False),
54
- ("", False),
55
- ],
56
- )
57
- def test_is_valid_python_name(name, expected):
58
- assert _is_valid_python_name(name) == expected
59
-
60
-
61
- def test_validate_hud_config_valid():
62
- config = {"function": "test.func", "args": ["arg1", "arg2"]}
63
- result = _validate_hud_config(config)
64
- assert result.function == "test.func"
65
- assert result.args == ["arg1", "arg2"]
66
- assert result.id is None
67
-
68
- # Test with single arg (not in a list)
69
- config = {"function": "test.func", "args": "arg1"}
70
- result = _validate_hud_config(config)
71
- assert result.function == "test.func"
72
- assert result.args == ["arg1"]
73
-
74
- # Test with ID
75
- config = {"function": "test.func", "args": [], "id": "test_id"}
76
- result = _validate_hud_config(config)
77
- assert result.id == "test_id"
78
-
79
-
80
- def test_validate_hud_config_invalid():
81
- with pytest.raises(ValueError, match="function must be a string"):
82
- _validate_hud_config({"args": []})
83
-
84
- with pytest.raises(ValueError, match="function must be a string"):
85
- _validate_hud_config({"function": 123, "args": []})
86
-
87
-
88
- def test_split_and_validate_path_valid():
89
- # none should raise
90
- _split_and_validate_path("module.submodule.function")
91
- _split_and_validate_path("function")
92
- _split_and_validate_path("Module_123.function_456")
93
-
94
-
95
- def test_split_and_validate_path_invalid():
96
- with pytest.raises(ValueError, match="Invalid Python identifier in path"):
97
- _split_and_validate_path("invalid-module.function")
98
-
99
-
100
- def test_is_list_of_configs():
101
- valid_list = [
102
- FunctionConfig(function="test1", args=[]),
103
- FunctionConfig(function="test2", args=["arg1"]),
104
- ]
105
- assert _is_list_of_configs(valid_list) is True
106
-
107
- # Empty list
108
- assert _is_list_of_configs([]) is True
109
-
110
- # Invalid: not a list
111
- assert _is_list_of_configs("not_a_list") is False
112
-
113
- # Invalid: list with non-FunctionConfig items
114
- invalid_list = [FunctionConfig(function="test", args=[]), "not_a_function_config"]
115
- assert _is_list_of_configs(invalid_list) is False
116
-
117
-
118
- def test_expand_config_errors():
119
- with pytest.raises(ValueError):
120
- empty_tuple = ()
121
- expand_config(empty_tuple) # type: ignore
122
-
123
- with pytest.raises(ValueError):
124
- invalid_tuple = (123, "arg1")
125
- expand_config(invalid_tuple) # type: ignore
126
-
127
- with pytest.raises(ValueError, match="Unknown configuration type"):
128
- invalid_value = 123
129
- expand_config(invalid_value) # type: ignore