hatch-xclam 0.7.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.
- hatch/__init__.py +21 -0
- hatch/cli_hatch.py +2748 -0
- hatch/environment_manager.py +1375 -0
- hatch/installers/__init__.py +25 -0
- hatch/installers/dependency_installation_orchestrator.py +636 -0
- hatch/installers/docker_installer.py +545 -0
- hatch/installers/hatch_installer.py +198 -0
- hatch/installers/installation_context.py +109 -0
- hatch/installers/installer_base.py +195 -0
- hatch/installers/python_installer.py +342 -0
- hatch/installers/registry.py +179 -0
- hatch/installers/system_installer.py +588 -0
- hatch/mcp_host_config/__init__.py +38 -0
- hatch/mcp_host_config/backup.py +458 -0
- hatch/mcp_host_config/host_management.py +572 -0
- hatch/mcp_host_config/models.py +602 -0
- hatch/mcp_host_config/reporting.py +181 -0
- hatch/mcp_host_config/strategies.py +513 -0
- hatch/package_loader.py +263 -0
- hatch/python_environment_manager.py +734 -0
- hatch/registry_explorer.py +171 -0
- hatch/registry_retriever.py +335 -0
- hatch/template_generator.py +179 -0
- hatch_xclam-0.7.0.dist-info/METADATA +150 -0
- hatch_xclam-0.7.0.dist-info/RECORD +93 -0
- hatch_xclam-0.7.0.dist-info/WHEEL +5 -0
- hatch_xclam-0.7.0.dist-info/entry_points.txt +2 -0
- hatch_xclam-0.7.0.dist-info/licenses/LICENSE +661 -0
- hatch_xclam-0.7.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/run_environment_tests.py +124 -0
- tests/test_cli_version.py +122 -0
- tests/test_data/packages/basic/base_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/basic/base_pkg/mcp_server.py +21 -0
- tests/test_data/packages/basic/base_pkg_v2/hatch_mcp_server.py +18 -0
- tests/test_data/packages/basic/base_pkg_v2/mcp_server.py +21 -0
- tests/test_data/packages/basic/utility_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/basic/utility_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/complex_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/complex_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/docker_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/docker_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/mixed_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/mixed_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/python_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/python_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/simple_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/simple_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/dependencies/system_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/dependencies/system_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/error_scenarios/circular_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/error_scenarios/circular_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/error_scenarios/circular_dep_pkg_b/hatch_mcp_server.py +18 -0
- tests/test_data/packages/error_scenarios/circular_dep_pkg_b/mcp_server.py +21 -0
- tests/test_data/packages/error_scenarios/invalid_dep_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/error_scenarios/invalid_dep_pkg/mcp_server.py +21 -0
- tests/test_data/packages/error_scenarios/version_conflict_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/error_scenarios/version_conflict_pkg/mcp_server.py +21 -0
- tests/test_data/packages/schema_versions/schema_v1_1_0_pkg/main.py +11 -0
- tests/test_data/packages/schema_versions/schema_v1_2_0_pkg/main.py +11 -0
- tests/test_data/packages/schema_versions/schema_v1_2_1_pkg/hatch_mcp_server.py +18 -0
- tests/test_data/packages/schema_versions/schema_v1_2_1_pkg/mcp_server.py +21 -0
- tests/test_data_utils.py +472 -0
- tests/test_dependency_orchestrator_consent.py +266 -0
- tests/test_docker_installer.py +524 -0
- tests/test_env_manip.py +991 -0
- tests/test_hatch_installer.py +179 -0
- tests/test_installer_base.py +221 -0
- tests/test_mcp_atomic_operations.py +276 -0
- tests/test_mcp_backup_integration.py +308 -0
- tests/test_mcp_cli_all_host_specific_args.py +303 -0
- tests/test_mcp_cli_backup_management.py +295 -0
- tests/test_mcp_cli_direct_management.py +453 -0
- tests/test_mcp_cli_discovery_listing.py +582 -0
- tests/test_mcp_cli_host_config_integration.py +823 -0
- tests/test_mcp_cli_package_management.py +360 -0
- tests/test_mcp_cli_partial_updates.py +859 -0
- tests/test_mcp_environment_integration.py +520 -0
- tests/test_mcp_host_config_backup.py +257 -0
- tests/test_mcp_host_configuration_manager.py +331 -0
- tests/test_mcp_host_registry_decorator.py +348 -0
- tests/test_mcp_pydantic_architecture_v4.py +603 -0
- tests/test_mcp_server_config_models.py +242 -0
- tests/test_mcp_server_config_type_field.py +221 -0
- tests/test_mcp_sync_functionality.py +316 -0
- tests/test_mcp_user_feedback_reporting.py +359 -0
- tests/test_non_tty_integration.py +281 -0
- tests/test_online_package_loader.py +202 -0
- tests/test_python_environment_manager.py +882 -0
- tests/test_python_installer.py +327 -0
- tests/test_registry.py +51 -0
- tests/test_registry_retriever.py +250 -0
- tests/test_system_installer.py +733 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""Unit tests for dependency installation orchestrator consent handling.
|
|
2
|
+
|
|
3
|
+
This module tests the user consent functionality in the dependency installation
|
|
4
|
+
orchestrator, focusing on TTY detection, environment variable support, and
|
|
5
|
+
error handling scenarios.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import unittest
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from unittest.mock import patch, MagicMock
|
|
12
|
+
from hatch.installers.dependency_installation_orchestrator import DependencyInstallerOrchestrator
|
|
13
|
+
from hatch.package_loader import HatchPackageLoader
|
|
14
|
+
from hatch_validator.registry.registry_service import RegistryService
|
|
15
|
+
from wobble.decorators import regression_test
|
|
16
|
+
from test_data_utils import NonTTYTestDataLoader
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestUserConsentHandling(unittest.TestCase):
|
|
20
|
+
"""Test user consent handling in dependency installation orchestrator."""
|
|
21
|
+
|
|
22
|
+
def setUp(self):
|
|
23
|
+
"""Set up test environment with centralized test data."""
|
|
24
|
+
# Create mock dependencies for orchestrator
|
|
25
|
+
self.mock_package_loader = MagicMock(spec=HatchPackageLoader)
|
|
26
|
+
self.mock_registry_data = {"registry_schema_version": "1.1.0", "repositories": []}
|
|
27
|
+
self.mock_registry_service = MagicMock(spec=RegistryService)
|
|
28
|
+
|
|
29
|
+
# Create orchestrator with mocked dependencies
|
|
30
|
+
self.orchestrator = DependencyInstallerOrchestrator(
|
|
31
|
+
package_loader=self.mock_package_loader,
|
|
32
|
+
registry_service=self.mock_registry_service,
|
|
33
|
+
registry_data=self.mock_registry_data
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
self.test_data = NonTTYTestDataLoader()
|
|
37
|
+
self.mock_install_plan = self.test_data.get_installation_plan("basic_python_plan")
|
|
38
|
+
self.logging_messages = self.test_data.get_logging_messages()
|
|
39
|
+
|
|
40
|
+
@regression_test
|
|
41
|
+
@patch('sys.stdin.isatty', return_value=True)
|
|
42
|
+
@patch('builtins.input', return_value='y')
|
|
43
|
+
def test_tty_environment_user_approves(self, mock_input, mock_isatty):
|
|
44
|
+
"""Test user consent approval in TTY environment."""
|
|
45
|
+
result = self.orchestrator._request_user_consent(self.mock_install_plan)
|
|
46
|
+
|
|
47
|
+
self.assertTrue(result)
|
|
48
|
+
mock_input.assert_called_once_with("\nProceed with installation? [y/N]: ")
|
|
49
|
+
mock_isatty.assert_called_once()
|
|
50
|
+
|
|
51
|
+
@regression_test
|
|
52
|
+
@patch('sys.stdin.isatty', return_value=True)
|
|
53
|
+
@patch('builtins.input', return_value='yes')
|
|
54
|
+
def test_tty_environment_user_approves_full_word(self, mock_input, mock_isatty):
|
|
55
|
+
"""Test user consent approval with 'yes' in TTY environment."""
|
|
56
|
+
result = self.orchestrator._request_user_consent(self.mock_install_plan)
|
|
57
|
+
|
|
58
|
+
self.assertTrue(result)
|
|
59
|
+
mock_input.assert_called_once_with("\nProceed with installation? [y/N]: ")
|
|
60
|
+
|
|
61
|
+
@regression_test
|
|
62
|
+
@patch('sys.stdin.isatty', return_value=True)
|
|
63
|
+
@patch('builtins.input', return_value='n')
|
|
64
|
+
def test_tty_environment_user_denies(self, mock_input, mock_isatty):
|
|
65
|
+
"""Test user consent denial in TTY environment."""
|
|
66
|
+
result = self.orchestrator._request_user_consent(self.mock_install_plan)
|
|
67
|
+
|
|
68
|
+
self.assertFalse(result)
|
|
69
|
+
mock_input.assert_called_once_with("\nProceed with installation? [y/N]: ")
|
|
70
|
+
|
|
71
|
+
@regression_test
|
|
72
|
+
@patch('sys.stdin.isatty', return_value=True)
|
|
73
|
+
@patch('builtins.input', return_value='no')
|
|
74
|
+
def test_tty_environment_user_denies_full_word(self, mock_input, mock_isatty):
|
|
75
|
+
"""Test user consent denial with 'no' in TTY environment."""
|
|
76
|
+
result = self.orchestrator._request_user_consent(self.mock_install_plan)
|
|
77
|
+
|
|
78
|
+
self.assertFalse(result)
|
|
79
|
+
mock_input.assert_called_once_with("\nProceed with installation? [y/N]: ")
|
|
80
|
+
|
|
81
|
+
@regression_test
|
|
82
|
+
@patch('sys.stdin.isatty', return_value=True)
|
|
83
|
+
@patch('builtins.input', return_value='')
|
|
84
|
+
def test_tty_environment_user_default_deny(self, mock_input, mock_isatty):
|
|
85
|
+
"""Test user consent default (empty) response in TTY environment."""
|
|
86
|
+
result = self.orchestrator._request_user_consent(self.mock_install_plan)
|
|
87
|
+
|
|
88
|
+
self.assertFalse(result)
|
|
89
|
+
mock_input.assert_called_once_with("\nProceed with installation? [y/N]: ")
|
|
90
|
+
|
|
91
|
+
@regression_test
|
|
92
|
+
@patch('sys.stdin.isatty', return_value=True)
|
|
93
|
+
@patch('builtins.input', side_effect=['invalid', 'y'])
|
|
94
|
+
@patch('builtins.print')
|
|
95
|
+
def test_tty_environment_invalid_then_valid_input(self, mock_print, mock_input, mock_isatty):
|
|
96
|
+
"""Test handling of invalid input followed by valid input."""
|
|
97
|
+
result = self.orchestrator._request_user_consent(self.mock_install_plan)
|
|
98
|
+
|
|
99
|
+
self.assertTrue(result)
|
|
100
|
+
self.assertEqual(mock_input.call_count, 2)
|
|
101
|
+
mock_print.assert_called_once_with("Please enter 'y' for yes or 'n' for no.")
|
|
102
|
+
|
|
103
|
+
@regression_test
|
|
104
|
+
@patch('sys.stdin.isatty', return_value=False)
|
|
105
|
+
def test_non_tty_environment_auto_approve(self, mock_isatty):
|
|
106
|
+
"""Test automatic approval in non-TTY environment."""
|
|
107
|
+
with patch.object(self.orchestrator.logger, 'info') as mock_log:
|
|
108
|
+
result = self.orchestrator._request_user_consent(self.mock_install_plan)
|
|
109
|
+
|
|
110
|
+
self.assertTrue(result)
|
|
111
|
+
mock_isatty.assert_called_once()
|
|
112
|
+
mock_log.assert_called_with(self.logging_messages["auto_approve"])
|
|
113
|
+
|
|
114
|
+
@regression_test
|
|
115
|
+
@patch('sys.stdin.isatty', return_value=True)
|
|
116
|
+
@patch.dict(os.environ, {'HATCH_AUTO_APPROVE': '1'})
|
|
117
|
+
def test_environment_variable_numeric_true(self, mock_isatty):
|
|
118
|
+
"""Test HATCH_AUTO_APPROVE=1 triggers auto-approval."""
|
|
119
|
+
with patch.object(self.orchestrator.logger, 'info') as mock_log:
|
|
120
|
+
result = self.orchestrator._request_user_consent(self.mock_install_plan)
|
|
121
|
+
|
|
122
|
+
self.assertTrue(result)
|
|
123
|
+
mock_log.assert_called_with(self.logging_messages["auto_approve"])
|
|
124
|
+
|
|
125
|
+
@regression_test
|
|
126
|
+
@patch('sys.stdin.isatty', return_value=True)
|
|
127
|
+
@patch.dict(os.environ, {'HATCH_AUTO_APPROVE': 'true'})
|
|
128
|
+
def test_environment_variable_string_true(self, mock_isatty):
|
|
129
|
+
"""Test HATCH_AUTO_APPROVE=true triggers auto-approval."""
|
|
130
|
+
with patch.object(self.orchestrator.logger, 'info') as mock_log:
|
|
131
|
+
result = self.orchestrator._request_user_consent(self.mock_install_plan)
|
|
132
|
+
|
|
133
|
+
self.assertTrue(result)
|
|
134
|
+
mock_log.assert_called_with(self.logging_messages["auto_approve"])
|
|
135
|
+
|
|
136
|
+
@regression_test
|
|
137
|
+
@patch('sys.stdin.isatty', return_value=True)
|
|
138
|
+
@patch.dict(os.environ, {'HATCH_AUTO_APPROVE': 'YES'})
|
|
139
|
+
def test_environment_variable_case_insensitive(self, mock_isatty):
|
|
140
|
+
"""Test HATCH_AUTO_APPROVE is case-insensitive."""
|
|
141
|
+
with patch.object(self.orchestrator.logger, 'info') as mock_log:
|
|
142
|
+
result = self.orchestrator._request_user_consent(self.mock_install_plan)
|
|
143
|
+
|
|
144
|
+
self.assertTrue(result)
|
|
145
|
+
mock_log.assert_called_with(self.logging_messages["auto_approve"])
|
|
146
|
+
|
|
147
|
+
@regression_test
|
|
148
|
+
@patch('sys.stdin.isatty', return_value=True)
|
|
149
|
+
@patch.dict(os.environ, {'HATCH_AUTO_APPROVE': 'invalid'})
|
|
150
|
+
@patch('builtins.input', return_value='y')
|
|
151
|
+
def test_environment_variable_invalid_value(self, mock_input, mock_isatty):
|
|
152
|
+
"""Test invalid HATCH_AUTO_APPROVE value falls back to TTY behavior."""
|
|
153
|
+
result = self.orchestrator._request_user_consent(self.mock_install_plan)
|
|
154
|
+
|
|
155
|
+
self.assertTrue(result)
|
|
156
|
+
mock_input.assert_called_once()
|
|
157
|
+
|
|
158
|
+
@regression_test
|
|
159
|
+
@patch('sys.stdin.isatty', return_value=True)
|
|
160
|
+
@patch('builtins.input', side_effect=EOFError())
|
|
161
|
+
def test_eof_error_handling(self, mock_input, mock_isatty):
|
|
162
|
+
"""Test EOFError handling in interactive mode."""
|
|
163
|
+
with patch.object(self.orchestrator.logger, 'info') as mock_log:
|
|
164
|
+
result = self.orchestrator._request_user_consent(self.mock_install_plan)
|
|
165
|
+
|
|
166
|
+
self.assertFalse(result)
|
|
167
|
+
mock_log.assert_called_with(self.logging_messages["user_cancelled"])
|
|
168
|
+
|
|
169
|
+
@regression_test
|
|
170
|
+
@patch('sys.stdin.isatty', return_value=True)
|
|
171
|
+
@patch('builtins.input', side_effect=KeyboardInterrupt())
|
|
172
|
+
def test_keyboard_interrupt_handling(self, mock_input, mock_isatty):
|
|
173
|
+
"""Test KeyboardInterrupt handling in interactive mode."""
|
|
174
|
+
with patch.object(self.orchestrator.logger, 'info') as mock_log:
|
|
175
|
+
result = self.orchestrator._request_user_consent(self.mock_install_plan)
|
|
176
|
+
|
|
177
|
+
self.assertFalse(result)
|
|
178
|
+
mock_log.assert_called_with(self.logging_messages["user_cancelled"])
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class TestEnvironmentVariableScenarios(unittest.TestCase):
|
|
182
|
+
"""Test comprehensive environment variable scenarios using centralized test data."""
|
|
183
|
+
|
|
184
|
+
def setUp(self):
|
|
185
|
+
"""Set up test environment with centralized test data."""
|
|
186
|
+
# Create mock dependencies for orchestrator
|
|
187
|
+
self.mock_package_loader = MagicMock(spec=HatchPackageLoader)
|
|
188
|
+
self.mock_registry_data = {"registry_schema_version": "1.1.0", "repositories": []}
|
|
189
|
+
self.mock_registry_service = MagicMock(spec=RegistryService)
|
|
190
|
+
|
|
191
|
+
# Create orchestrator with mocked dependencies
|
|
192
|
+
self.orchestrator = DependencyInstallerOrchestrator(
|
|
193
|
+
package_loader=self.mock_package_loader,
|
|
194
|
+
registry_service=self.mock_registry_service,
|
|
195
|
+
registry_data=self.mock_registry_data
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
self.test_data = NonTTYTestDataLoader()
|
|
199
|
+
self.mock_install_plan = self.test_data.get_installation_plan("basic_python_plan")
|
|
200
|
+
self.env_scenarios = self.test_data.get_environment_variable_scenarios()
|
|
201
|
+
self.logging_messages = self.test_data.get_logging_messages()
|
|
202
|
+
|
|
203
|
+
@regression_test
|
|
204
|
+
@patch('sys.stdin.isatty', return_value=True)
|
|
205
|
+
@patch('builtins.input', return_value='n') # Mock input for fallback cases to deny
|
|
206
|
+
def test_all_environment_variable_scenarios(self, mock_input, mock_isatty):
|
|
207
|
+
"""Test all environment variable scenarios from centralized test data."""
|
|
208
|
+
for scenario in self.env_scenarios:
|
|
209
|
+
with self.subTest(scenario=scenario["name"]):
|
|
210
|
+
with patch.dict(os.environ, {'HATCH_AUTO_APPROVE': scenario["value"]}):
|
|
211
|
+
with patch.object(self.orchestrator.logger, 'info') as mock_log:
|
|
212
|
+
result = self.orchestrator._request_user_consent(self.mock_install_plan)
|
|
213
|
+
|
|
214
|
+
self.assertEqual(result, scenario["expected"],
|
|
215
|
+
f"Failed for scenario: {scenario['name']} with value: {scenario['value']}")
|
|
216
|
+
|
|
217
|
+
if scenario["expected"]:
|
|
218
|
+
mock_log.assert_called_with(self.logging_messages["auto_approve"])
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class TestInstallationPlanVariations(unittest.TestCase):
|
|
222
|
+
"""Test consent handling with different installation plan variations."""
|
|
223
|
+
|
|
224
|
+
def setUp(self):
|
|
225
|
+
"""Set up test environment with centralized test data."""
|
|
226
|
+
# Create mock dependencies for orchestrator
|
|
227
|
+
self.mock_package_loader = MagicMock(spec=HatchPackageLoader)
|
|
228
|
+
self.mock_registry_data = {"registry_schema_version": "1.1.0", "repositories": []}
|
|
229
|
+
self.mock_registry_service = MagicMock(spec=RegistryService)
|
|
230
|
+
|
|
231
|
+
# Create orchestrator with mocked dependencies
|
|
232
|
+
self.orchestrator = DependencyInstallerOrchestrator(
|
|
233
|
+
package_loader=self.mock_package_loader,
|
|
234
|
+
registry_service=self.mock_registry_service,
|
|
235
|
+
registry_data=self.mock_registry_data
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
self.test_data = NonTTYTestDataLoader()
|
|
239
|
+
|
|
240
|
+
@regression_test
|
|
241
|
+
@patch('sys.stdin.isatty', return_value=False)
|
|
242
|
+
def test_non_tty_with_empty_plan(self, mock_isatty):
|
|
243
|
+
"""Test non-TTY behavior with empty installation plan."""
|
|
244
|
+
empty_plan = self.test_data.get_installation_plan("empty_plan")
|
|
245
|
+
|
|
246
|
+
with patch.object(self.orchestrator.logger, 'info') as mock_log:
|
|
247
|
+
result = self.orchestrator._request_user_consent(empty_plan)
|
|
248
|
+
|
|
249
|
+
self.assertTrue(result)
|
|
250
|
+
mock_log.assert_called_with(self.test_data.get_logging_messages()["auto_approve"])
|
|
251
|
+
|
|
252
|
+
@regression_test
|
|
253
|
+
@patch('sys.stdin.isatty', return_value=False)
|
|
254
|
+
def test_non_tty_with_complex_plan(self, mock_isatty):
|
|
255
|
+
"""Test non-TTY behavior with complex installation plan."""
|
|
256
|
+
complex_plan = self.test_data.get_installation_plan("complex_plan")
|
|
257
|
+
|
|
258
|
+
with patch.object(self.orchestrator.logger, 'info') as mock_log:
|
|
259
|
+
result = self.orchestrator._request_user_consent(complex_plan)
|
|
260
|
+
|
|
261
|
+
self.assertTrue(result)
|
|
262
|
+
mock_log.assert_called_with(self.test_data.get_logging_messages()["auto_approve"])
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
if __name__ == '__main__':
|
|
266
|
+
unittest.main()
|