hud-python 0.4.11__py3-none-any.whl → 0.4.12__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 (63) hide show
  1. hud/__main__.py +8 -0
  2. hud/agents/base.py +7 -8
  3. hud/agents/langchain.py +2 -2
  4. hud/agents/tests/test_openai.py +3 -1
  5. hud/cli/__init__.py +106 -51
  6. hud/cli/build.py +121 -71
  7. hud/cli/debug.py +2 -2
  8. hud/cli/{mcp_server.py → dev.py} +60 -25
  9. hud/cli/eval.py +148 -68
  10. hud/cli/init.py +0 -1
  11. hud/cli/list_func.py +72 -71
  12. hud/cli/pull.py +1 -2
  13. hud/cli/push.py +35 -23
  14. hud/cli/remove.py +35 -41
  15. hud/cli/tests/test_analyze.py +2 -1
  16. hud/cli/tests/test_analyze_metadata.py +42 -49
  17. hud/cli/tests/test_build.py +28 -52
  18. hud/cli/tests/test_cursor.py +1 -1
  19. hud/cli/tests/test_debug.py +1 -1
  20. hud/cli/tests/test_list_func.py +75 -64
  21. hud/cli/tests/test_main_module.py +30 -0
  22. hud/cli/tests/test_mcp_server.py +3 -3
  23. hud/cli/tests/test_pull.py +30 -61
  24. hud/cli/tests/test_push.py +70 -89
  25. hud/cli/tests/test_registry.py +36 -38
  26. hud/cli/tests/test_utils.py +1 -1
  27. hud/cli/utils/__init__.py +1 -0
  28. hud/cli/{docker_utils.py → utils/docker.py} +36 -0
  29. hud/cli/{env_utils.py → utils/environment.py} +7 -7
  30. hud/cli/{interactive.py → utils/interactive.py} +91 -19
  31. hud/cli/{analyze_metadata.py → utils/metadata.py} +12 -8
  32. hud/cli/{registry.py → utils/registry.py} +28 -30
  33. hud/cli/{remote_runner.py → utils/remote_runner.py} +1 -1
  34. hud/cli/utils/runner.py +134 -0
  35. hud/cli/utils/server.py +250 -0
  36. hud/clients/base.py +1 -1
  37. hud/clients/fastmcp.py +7 -5
  38. hud/clients/mcp_use.py +8 -6
  39. hud/server/server.py +34 -4
  40. hud/shared/exceptions.py +11 -0
  41. hud/shared/tests/test_exceptions.py +22 -0
  42. hud/telemetry/tests/__init__.py +0 -0
  43. hud/telemetry/tests/test_replay.py +40 -0
  44. hud/telemetry/tests/test_trace.py +63 -0
  45. hud/tools/base.py +20 -3
  46. hud/tools/computer/hud.py +15 -6
  47. hud/tools/executors/tests/test_base_executor.py +27 -0
  48. hud/tools/response.py +12 -8
  49. hud/tools/tests/test_response.py +60 -0
  50. hud/tools/tests/test_tools_init.py +49 -0
  51. hud/utils/design.py +19 -8
  52. hud/utils/mcp.py +17 -5
  53. hud/utils/tests/test_mcp.py +112 -0
  54. hud/utils/tests/test_version.py +1 -1
  55. hud/version.py +1 -1
  56. {hud_python-0.4.11.dist-info → hud_python-0.4.12.dist-info}/METADATA +14 -10
  57. {hud_python-0.4.11.dist-info → hud_python-0.4.12.dist-info}/RECORD +62 -52
  58. hud/cli/runner.py +0 -160
  59. /hud/cli/{cursor.py → utils/cursor.py} +0 -0
  60. /hud/cli/{utils.py → utils/logging.py} +0 -0
  61. {hud_python-0.4.11.dist-info → hud_python-0.4.12.dist-info}/WHEEL +0 -0
  62. {hud_python-0.4.11.dist-info → hud_python-0.4.12.dist-info}/entry_points.txt +0 -0
  63. {hud_python-0.4.11.dist-info → hud_python-0.4.12.dist-info}/licenses/LICENSE +0 -0
@@ -3,8 +3,6 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
- import subprocess
7
- from pathlib import Path
8
6
  from unittest import mock
9
7
 
10
8
  import pytest
@@ -30,22 +28,14 @@ class TestGetDockerManifest:
30
28
  manifest_data = {
31
29
  "schemaVersion": 2,
32
30
  "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
33
- "layers": [
34
- {"size": 1024},
35
- {"size": 2048}
36
- ]
31
+ "layers": [{"size": 1024}, {"size": 2048}],
37
32
  }
38
- mock_run.return_value = mock.Mock(
39
- returncode=0,
40
- stdout=json.dumps(manifest_data)
41
- )
33
+ mock_run.return_value = mock.Mock(returncode=0, stdout=json.dumps(manifest_data))
42
34
 
43
35
  result = get_docker_manifest("test:latest")
44
36
  assert result == manifest_data
45
37
  mock_run.assert_called_once_with(
46
- ["docker", "manifest", "inspect", "test:latest"],
47
- capture_output=True,
48
- text=True
38
+ ["docker", "manifest", "inspect", "test:latest"], capture_output=True, text=True
49
39
  )
50
40
 
51
41
  @mock.patch("subprocess.run")
@@ -70,14 +60,8 @@ class TestGetImageSizeFromManifest:
70
60
 
71
61
  def test_get_size_v2_manifest(self):
72
62
  """Test getting size from v2 manifest with layers."""
73
- manifest = {
74
- "layers": [
75
- {"size": 1024},
76
- {"size": 2048},
77
- {"size": 512}
78
- ]
79
- }
80
-
63
+ manifest = {"layers": [{"size": 1024}, {"size": 2048}, {"size": 512}]}
64
+
81
65
  size = get_image_size_from_manifest(manifest)
82
66
  assert size == 3584 # Sum of all layers
83
67
 
@@ -86,24 +70,24 @@ class TestGetImageSizeFromManifest:
86
70
  manifest = {
87
71
  "manifests": [
88
72
  {"size": 5120, "platform": {"os": "linux"}},
89
- {"size": 4096, "platform": {"os": "windows"}}
73
+ {"size": 4096, "platform": {"os": "windows"}},
90
74
  ]
91
75
  }
92
-
76
+
93
77
  size = get_image_size_from_manifest(manifest)
94
78
  assert size == 5120 # First manifest size
95
79
 
96
80
  def test_get_size_empty_manifest(self):
97
81
  """Test getting size from empty manifest."""
98
82
  manifest = {}
99
-
83
+
100
84
  size = get_image_size_from_manifest(manifest)
101
85
  assert size is None
102
86
 
103
87
  def test_get_size_invalid_manifest(self):
104
88
  """Test getting size from invalid manifest."""
105
89
  manifest = {"invalid": "data"}
106
-
90
+
107
91
  size = get_image_size_from_manifest(manifest)
108
92
  assert size is None
109
93
 
@@ -132,24 +116,24 @@ class TestFetchLockFromRegistry:
132
116
  mock_get.return_value = mock_response
133
117
 
134
118
  fetch_lock_from_registry("org/env")
135
-
136
- # Check URL includes :latest
119
+
120
+ # Check URL includes :latest (URL-encoded)
137
121
  call_args = mock_get.call_args
138
- assert "org/env:latest" in call_args[0][0]
122
+ assert "org/env%3Alatest" in call_args[0][0]
139
123
 
140
124
  @mock.patch("hud.cli.pull.settings")
141
125
  @mock.patch("requests.get")
142
126
  def test_fetch_lock_with_auth(self, mock_get, mock_settings):
143
127
  """Test fetching with API key."""
144
128
  mock_settings.api_key = "test-key"
145
-
129
+
146
130
  mock_response = mock.Mock()
147
131
  mock_response.status_code = 200
148
132
  mock_response.json.return_value = {"test": "data"}
149
133
  mock_get.return_value = mock_response
150
134
 
151
135
  fetch_lock_from_registry("org/env:latest")
152
-
136
+
153
137
  # Check auth header was set
154
138
  call_kwargs = mock_get.call_args[1]
155
139
  assert call_kwargs["headers"]["Authorization"] == "Bearer test-key"
@@ -205,21 +189,13 @@ class TestPullEnvironment:
205
189
  mock_design = mock.Mock()
206
190
  mock_design.console = mock.Mock()
207
191
  mock_design_class.return_value = mock_design
208
-
192
+
209
193
  # Create lock file
210
194
  lock_data = {
211
195
  "image": "test/env:latest@sha256:abc123",
212
- "build": {
213
- "generatedAt": "2024-01-01T00:00:00Z",
214
- "hudVersion": "1.0.0"
215
- },
216
- "environment": {
217
- "toolCount": 5,
218
- "initializeMs": 1500
219
- },
220
- "tools": [
221
- {"name": "tool1", "description": "Tool 1"}
222
- ]
196
+ "build": {"generatedAt": "2024-01-01T00:00:00Z", "hudVersion": "1.0.0"},
197
+ "environment": {"toolCount": 5, "initializeMs": 1500},
198
+ "tools": [{"name": "tool1", "description": "Tool 1"}],
223
199
  }
224
200
  lock_file = tmp_path / "hud.lock.yaml"
225
201
  lock_file.write_text(yaml.dump(lock_data))
@@ -251,12 +227,9 @@ class TestPullEnvironment:
251
227
  mock_design = mock.Mock()
252
228
  mock_design.console = mock.Mock()
253
229
  mock_design_class.return_value = mock_design
254
-
230
+
255
231
  # Mock registry response
256
- lock_data = {
257
- "image": "docker.io/org/env:latest@sha256:def456",
258
- "tools": []
259
- }
232
+ lock_data = {"image": "docker.io/org/env:latest@sha256:def456", "tools": []}
260
233
  mock_fetch.return_value = lock_data
261
234
 
262
235
  # Mock docker pull
@@ -281,20 +254,20 @@ class TestPullEnvironment:
281
254
  @mock.patch("hud.cli.pull.get_docker_manifest")
282
255
  @mock.patch("hud.cli.pull.fetch_lock_from_registry")
283
256
  @mock.patch("subprocess.Popen")
284
- def test_pull_docker_image_direct(self, mock_popen, mock_fetch, mock_manifest, mock_design_class):
257
+ def test_pull_docker_image_direct(
258
+ self, mock_popen, mock_fetch, mock_manifest, mock_design_class
259
+ ):
285
260
  """Test pulling Docker image directly."""
286
261
  # Create mock design instance
287
262
  mock_design = mock.Mock()
288
263
  mock_design.console = mock.Mock()
289
264
  mock_design_class.return_value = mock_design
290
-
265
+
291
266
  # Mock no registry data
292
267
  mock_fetch.return_value = None
293
268
 
294
269
  # Mock manifest
295
- mock_manifest.return_value = {
296
- "layers": [{"size": 1024}]
297
- }
270
+ mock_manifest.return_value = {"layers": [{"size": 1024}]}
298
271
 
299
272
  # Mock docker pull
300
273
  mock_process = mock.Mock()
@@ -318,7 +291,7 @@ class TestPullEnvironment:
318
291
  mock_design = mock.Mock()
319
292
  mock_design.console = mock.Mock()
320
293
  mock_design_class.return_value = mock_design
321
-
294
+
322
295
  # Should not actually pull
323
296
  pull_environment("test:latest", verify_only=True)
324
297
 
@@ -333,7 +306,7 @@ class TestPullEnvironment:
333
306
  mock_design = mock.Mock()
334
307
  mock_design.console = mock.Mock()
335
308
  mock_design_class.return_value = mock_design
336
-
309
+
337
310
  # Mock docker pull failure
338
311
  mock_process = mock.Mock()
339
312
  mock_process.stdout = ["Error: manifest unknown\n"]
@@ -355,7 +328,7 @@ class TestPullEnvironment:
355
328
  mock_design = mock.Mock()
356
329
  mock_design.console = mock.Mock()
357
330
  mock_design_class.return_value = mock_design
358
-
331
+
359
332
  mock_confirm.return_value = False
360
333
 
361
334
  with pytest.raises(typer.Exit) as exc_info:
@@ -371,7 +344,7 @@ class TestPullEnvironment:
371
344
  mock_design = mock.Mock()
372
345
  mock_design.console = mock.Mock()
373
346
  mock_design_class.return_value = mock_design
374
-
347
+
375
348
  with pytest.raises(typer.Exit):
376
349
  pull_environment("nonexistent.yaml")
377
350
 
@@ -392,9 +365,5 @@ class TestPullCommand:
392
365
  # Just test it doesn't crash with explicit values
393
366
  with mock.patch("hud.cli.pull.pull_environment"):
394
367
  pull_command(
395
- "org/env:v1.0",
396
- lock_file="lock.yaml",
397
- yes=True,
398
- verify_only=True,
399
- verbose=True
368
+ "org/env:v1.0", lock_file="lock.yaml", yes=True, verify_only=True, verbose=True
400
369
  )
@@ -5,12 +5,9 @@ from __future__ import annotations
5
5
  import base64
6
6
  import json
7
7
  import subprocess
8
- from datetime import datetime
9
- from pathlib import Path
10
8
  from unittest import mock
11
9
 
12
10
  import pytest
13
- import requests
14
11
  import typer
15
12
  import yaml
16
13
 
@@ -30,7 +27,7 @@ class TestGetDockerUsername:
30
27
  # Create mock Docker config
31
28
  docker_dir = tmp_path / ".docker"
32
29
  docker_dir.mkdir()
33
-
30
+
34
31
  config_file = docker_dir / "config.json"
35
32
  config = {
36
33
  "auths": {
@@ -40,37 +37,31 @@ class TestGetDockerUsername:
40
37
  }
41
38
  }
42
39
  config_file.write_text(json.dumps(config))
43
-
40
+
44
41
  with mock.patch("pathlib.Path.home", return_value=tmp_path):
45
42
  username = get_docker_username()
46
-
43
+
47
44
  assert username == "testuser"
48
45
 
49
46
  def test_get_username_no_config(self, tmp_path):
50
47
  """Test when no Docker config exists."""
51
48
  with mock.patch("pathlib.Path.home", return_value=tmp_path):
52
49
  username = get_docker_username()
53
-
50
+
54
51
  assert username is None
55
52
 
56
53
  def test_get_username_token_auth(self, tmp_path):
57
54
  """Test skipping token-based auth."""
58
55
  docker_dir = tmp_path / ".docker"
59
56
  docker_dir.mkdir()
60
-
57
+
61
58
  config_file = docker_dir / "config.json"
62
- config = {
63
- "auths": {
64
- "docker.io": {
65
- "auth": base64.b64encode(b"token:xyz").decode()
66
- }
67
- }
68
- }
59
+ config = {"auths": {"docker.io": {"auth": base64.b64encode(b"token:xyz").decode()}}}
69
60
  config_file.write_text(json.dumps(config))
70
-
61
+
71
62
  with mock.patch("pathlib.Path.home", return_value=tmp_path):
72
63
  username = get_docker_username()
73
-
64
+
74
65
  assert username is None
75
66
 
76
67
  @mock.patch("subprocess.run")
@@ -78,20 +69,20 @@ class TestGetDockerUsername:
78
69
  """Test getting username from credential helper."""
79
70
  docker_dir = tmp_path / ".docker"
80
71
  docker_dir.mkdir()
81
-
72
+
82
73
  config_file = docker_dir / "config.json"
83
74
  config = {"credsStore": "desktop"}
84
75
  config_file.write_text(json.dumps(config))
85
-
76
+
86
77
  # Mock credential helper calls
87
78
  mock_run.side_effect = [
88
79
  mock.Mock(returncode=0, stdout='{"https://index.docker.io/v1/": "creds"}'),
89
- mock.Mock(returncode=0, stdout='{"Username": "helperuser", "Secret": "pass"}')
80
+ mock.Mock(returncode=0, stdout='{"Username": "helperuser", "Secret": "pass"}'),
90
81
  ]
91
-
82
+
92
83
  with mock.patch("pathlib.Path.home", return_value=tmp_path):
93
84
  username = get_docker_username()
94
-
85
+
95
86
  assert username == "helperuser"
96
87
 
97
88
 
@@ -101,16 +92,9 @@ class TestGetDockerImageLabels:
101
92
  @mock.patch("subprocess.run")
102
93
  def test_get_labels_success(self, mock_run):
103
94
  """Test successfully getting image labels."""
104
- labels = {
105
- "org.hud.manifest.head": "abc123",
106
- "org.hud.version": "1.0.0"
107
- }
108
- mock_run.return_value = mock.Mock(
109
- returncode=0,
110
- stdout=json.dumps(labels),
111
- stderr=""
112
- )
113
-
95
+ labels = {"org.hud.manifest.head": "abc123", "org.hud.version": "1.0.0"}
96
+ mock_run.return_value = mock.Mock(returncode=0, stdout=json.dumps(labels), stderr="")
97
+
114
98
  result = get_docker_image_labels("test:latest")
115
99
  assert result == labels
116
100
 
@@ -118,7 +102,7 @@ class TestGetDockerImageLabels:
118
102
  def test_get_labels_failure(self, mock_run):
119
103
  """Test handling failure to get labels."""
120
104
  mock_run.side_effect = Exception("Command failed")
121
-
105
+
122
106
  result = get_docker_image_labels("test:latest")
123
107
  assert result == {}
124
108
 
@@ -131,10 +115,10 @@ class TestPushEnvironment:
131
115
  """Test pushing when no lock file exists."""
132
116
  mock_design = mock.Mock()
133
117
  mock_design_class.return_value = mock_design
134
-
118
+
135
119
  with pytest.raises(typer.Exit) as exc_info:
136
120
  push_environment(str(tmp_path))
137
-
121
+
138
122
  assert exc_info.value.exit_code == 1
139
123
  mock_design.error.assert_called()
140
124
 
@@ -145,14 +129,14 @@ class TestPushEnvironment:
145
129
  mock_design = mock.Mock()
146
130
  mock_design_class.return_value = mock_design
147
131
  mock_settings.api_key = None
148
-
132
+
149
133
  # Create lock file
150
134
  lock_file = tmp_path / "hud.lock.yaml"
151
135
  lock_file.write_text(yaml.dump({"image": "test:latest"}))
152
-
136
+
153
137
  with pytest.raises(typer.Exit) as exc_info:
154
138
  push_environment(str(tmp_path))
155
-
139
+
156
140
  assert exc_info.value.exit_code == 1
157
141
 
158
142
  @mock.patch("requests.post")
@@ -162,8 +146,14 @@ class TestPushEnvironment:
162
146
  @mock.patch("hud.cli.push.settings")
163
147
  @mock.patch("hud.cli.push.HUDDesign")
164
148
  def test_push_auto_detect_username(
165
- self, mock_design_class, mock_settings, mock_get_username,
166
- mock_run, mock_popen, mock_post, tmp_path
149
+ self,
150
+ mock_design_class,
151
+ mock_settings,
152
+ mock_get_username,
153
+ mock_run,
154
+ mock_popen,
155
+ mock_post,
156
+ tmp_path,
167
157
  ):
168
158
  """Test auto-detecting Docker username and pushing."""
169
159
  # Setup mocks
@@ -172,15 +162,12 @@ class TestPushEnvironment:
172
162
  mock_settings.api_key = "test-key"
173
163
  mock_settings.hud_telemetry_url = "https://api.hud.test"
174
164
  mock_get_username.return_value = "testuser"
175
-
165
+
176
166
  # Create lock file
177
- lock_data = {
178
- "image": "original/image:v1.0",
179
- "build": {"version": "0.1.0"}
180
- }
167
+ lock_data = {"image": "original/image:v1.0", "build": {"version": "0.1.0"}}
181
168
  lock_file = tmp_path / "hud.lock.yaml"
182
169
  lock_file.write_text(yaml.dump(lock_data))
183
-
170
+
184
171
  # Mock docker commands
185
172
  def mock_run_impl(*args, **kwargs):
186
173
  cmd = args[0]
@@ -192,50 +179,48 @@ class TestPushEnvironment:
192
179
  elif cmd[1] == "tag":
193
180
  return mock.Mock(returncode=0)
194
181
  return mock.Mock(returncode=0)
195
-
182
+
196
183
  mock_run.side_effect = mock_run_impl
197
-
184
+
198
185
  # Mock docker push
199
186
  mock_process = mock.Mock()
200
187
  mock_process.stdout = ["Pushing image...", "Push complete"]
201
188
  mock_process.wait.return_value = None
202
189
  mock_process.returncode = 0
203
190
  mock_popen.return_value = mock_process
204
-
191
+
205
192
  # Mock registry upload
206
193
  mock_post.return_value = mock.Mock(status_code=201)
207
-
194
+
208
195
  # Run push
209
196
  push_environment(str(tmp_path), yes=True)
210
-
197
+
211
198
  # Verify docker commands
212
199
  assert mock_run.call_count >= 2
213
200
  mock_popen.assert_called_once()
214
-
201
+
215
202
  # Verify registry upload
216
203
  mock_post.assert_called_once()
217
204
  call_args = mock_post.call_args
218
- assert "testuser/image:0.1.0" in call_args[0][0]
205
+ assert "testuser/image%3A0.1.0" in call_args[0][0]
219
206
 
220
207
  @mock.patch("subprocess.run")
221
208
  @mock.patch("hud.cli.push.settings")
222
209
  @mock.patch("hud.cli.push.HUDDesign")
223
- def test_push_explicit_image(
224
- self, mock_design_class, mock_settings, mock_run, tmp_path
225
- ):
210
+ def test_push_explicit_image(self, mock_design_class, mock_settings, mock_run, tmp_path):
226
211
  """Test pushing with explicit image name."""
227
212
  mock_design = mock.Mock()
228
213
  mock_design_class.return_value = mock_design
229
214
  mock_settings.api_key = "test-key"
230
-
215
+
231
216
  # Create lock file
232
217
  lock_data = {"image": "local:latest"}
233
218
  lock_file = tmp_path / "hud.lock.yaml"
234
219
  lock_file.write_text(yaml.dump(lock_data))
235
-
220
+
236
221
  # Mock docker inspect for non-existent local image
237
222
  mock_run.side_effect = subprocess.CalledProcessError(1, "docker")
238
-
223
+
239
224
  with pytest.raises(typer.Exit):
240
225
  push_environment(str(tmp_path), image="myrepo/myimage:v2")
241
226
 
@@ -243,19 +228,17 @@ class TestPushEnvironment:
243
228
  @mock.patch("subprocess.run")
244
229
  @mock.patch("hud.cli.push.settings")
245
230
  @mock.patch("hud.cli.push.HUDDesign")
246
- def test_push_with_tag(
247
- self, mock_design_class, mock_settings, mock_run, mock_popen, tmp_path
248
- ):
231
+ def test_push_with_tag(self, mock_design_class, mock_settings, mock_run, mock_popen, tmp_path):
249
232
  """Test pushing with explicit tag."""
250
233
  mock_design = mock.Mock()
251
234
  mock_design_class.return_value = mock_design
252
235
  mock_settings.api_key = "test-key"
253
-
236
+
254
237
  # Create lock file
255
238
  lock_data = {"image": "test:latest"}
256
239
  lock_file = tmp_path / "hud.lock.yaml"
257
240
  lock_file.write_text(yaml.dump(lock_data))
258
-
241
+
259
242
  # Mock docker commands
260
243
  def mock_run_impl(*args, **kwargs):
261
244
  cmd = args[0]
@@ -267,19 +250,19 @@ class TestPushEnvironment:
267
250
  elif cmd[1] == "tag":
268
251
  return mock.Mock(returncode=0)
269
252
  return mock.Mock(returncode=0)
270
-
253
+
271
254
  mock_run.side_effect = mock_run_impl
272
-
255
+
273
256
  # Mock docker push
274
257
  mock_process = mock.Mock()
275
258
  mock_process.stdout = []
276
259
  mock_process.wait.return_value = None
277
260
  mock_process.returncode = 0
278
261
  mock_popen.return_value = mock_process
279
-
262
+
280
263
  # Run push
281
264
  push_environment(str(tmp_path), image="user/test", tag="v2.0", yes=True)
282
-
265
+
283
266
  # Verify tag was used
284
267
  tag_call = [c for c in mock_run.call_args_list if c[0][0][1] == "tag"]
285
268
  assert len(tag_call) > 0
@@ -291,19 +274,21 @@ class TestPushEnvironment:
291
274
  """Test handling Docker push failure."""
292
275
  mock_design = mock.Mock()
293
276
  mock_design_class.return_value = mock_design
294
-
277
+
295
278
  # Mock docker push failure
296
279
  mock_process = mock.Mock()
297
280
  mock_process.stdout = ["Error: access denied"]
298
281
  mock_process.wait.return_value = None
299
282
  mock_process.returncode = 1
300
283
  mock_popen.return_value = mock_process
301
-
284
+
302
285
  with mock.patch("hud.cli.push.settings") as mock_settings:
303
286
  mock_settings.api_key = "test-key"
304
- with mock.patch("subprocess.run"):
305
- with pytest.raises(typer.Exit):
306
- push_environment(".", image="test:latest", yes=True)
287
+ with (
288
+ mock.patch("subprocess.run"),
289
+ pytest.raises(typer.Exit),
290
+ ):
291
+ push_environment(".", image="test:latest", yes=True)
307
292
 
308
293
  @mock.patch("hud.cli.push.get_docker_image_labels")
309
294
  @mock.patch("subprocess.run")
@@ -316,18 +301,18 @@ class TestPushEnvironment:
316
301
  mock_design = mock.Mock()
317
302
  mock_design_class.return_value = mock_design
318
303
  mock_settings.api_key = "test-key"
319
-
304
+
320
305
  # Create lock file
321
306
  lock_data = {"image": "test:latest"}
322
307
  lock_file = tmp_path / "hud.lock.yaml"
323
308
  lock_file.write_text(yaml.dump(lock_data))
324
-
309
+
325
310
  # Mock labels
326
311
  mock_get_labels.return_value = {
327
312
  "org.hud.manifest.head": "abc123def456",
328
- "org.hud.version": "1.2.3"
313
+ "org.hud.version": "1.2.3",
329
314
  }
330
-
315
+
331
316
  # Mock docker commands - first inspect succeeds to get to label check
332
317
  # Provide explicit image to bypass username check
333
318
  def mock_run_impl(*args, **kwargs):
@@ -339,13 +324,13 @@ class TestPushEnvironment:
339
324
  # Fail on tag to exit after labels are checked
340
325
  raise subprocess.CalledProcessError(1, cmd)
341
326
  return mock.Mock(returncode=0)
342
-
327
+
343
328
  mock_run.side_effect = mock_run_impl
344
-
329
+
345
330
  # Provide explicit image to ensure we reach label check
346
331
  with pytest.raises(subprocess.CalledProcessError):
347
332
  push_environment(str(tmp_path), image="test:v2", verbose=True)
348
-
333
+
349
334
  # Verify labels were checked
350
335
  mock_get_labels.assert_called_once_with("test:latest")
351
336
 
@@ -357,10 +342,8 @@ class TestPushCommand:
357
342
  """Test basic push command."""
358
343
  with mock.patch("hud.cli.push.push_environment") as mock_push:
359
344
  push_command()
360
-
361
- mock_push.assert_called_once_with(
362
- ".", None, None, False, False, False
363
- )
345
+
346
+ mock_push.assert_called_once_with(".", None, None, False, False, False)
364
347
 
365
348
  def test_push_command_with_options(self):
366
349
  """Test push command with all options."""
@@ -371,9 +354,7 @@ class TestPushCommand:
371
354
  tag="v1.0",
372
355
  sign=True,
373
356
  yes=True,
374
- verbose=True
375
- )
376
-
377
- mock_push.assert_called_once_with(
378
- "./myenv", "myrepo/myimage", "v1.0", True, True, True
357
+ verbose=True,
379
358
  )
359
+
360
+ mock_push.assert_called_once_with("./myenv", "myrepo/myimage", "v1.0", True, True, True)