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.
Files changed (93) hide show
  1. hatch/__init__.py +21 -0
  2. hatch/cli_hatch.py +2748 -0
  3. hatch/environment_manager.py +1375 -0
  4. hatch/installers/__init__.py +25 -0
  5. hatch/installers/dependency_installation_orchestrator.py +636 -0
  6. hatch/installers/docker_installer.py +545 -0
  7. hatch/installers/hatch_installer.py +198 -0
  8. hatch/installers/installation_context.py +109 -0
  9. hatch/installers/installer_base.py +195 -0
  10. hatch/installers/python_installer.py +342 -0
  11. hatch/installers/registry.py +179 -0
  12. hatch/installers/system_installer.py +588 -0
  13. hatch/mcp_host_config/__init__.py +38 -0
  14. hatch/mcp_host_config/backup.py +458 -0
  15. hatch/mcp_host_config/host_management.py +572 -0
  16. hatch/mcp_host_config/models.py +602 -0
  17. hatch/mcp_host_config/reporting.py +181 -0
  18. hatch/mcp_host_config/strategies.py +513 -0
  19. hatch/package_loader.py +263 -0
  20. hatch/python_environment_manager.py +734 -0
  21. hatch/registry_explorer.py +171 -0
  22. hatch/registry_retriever.py +335 -0
  23. hatch/template_generator.py +179 -0
  24. hatch_xclam-0.7.0.dist-info/METADATA +150 -0
  25. hatch_xclam-0.7.0.dist-info/RECORD +93 -0
  26. hatch_xclam-0.7.0.dist-info/WHEEL +5 -0
  27. hatch_xclam-0.7.0.dist-info/entry_points.txt +2 -0
  28. hatch_xclam-0.7.0.dist-info/licenses/LICENSE +661 -0
  29. hatch_xclam-0.7.0.dist-info/top_level.txt +2 -0
  30. tests/__init__.py +1 -0
  31. tests/run_environment_tests.py +124 -0
  32. tests/test_cli_version.py +122 -0
  33. tests/test_data/packages/basic/base_pkg/hatch_mcp_server.py +18 -0
  34. tests/test_data/packages/basic/base_pkg/mcp_server.py +21 -0
  35. tests/test_data/packages/basic/base_pkg_v2/hatch_mcp_server.py +18 -0
  36. tests/test_data/packages/basic/base_pkg_v2/mcp_server.py +21 -0
  37. tests/test_data/packages/basic/utility_pkg/hatch_mcp_server.py +18 -0
  38. tests/test_data/packages/basic/utility_pkg/mcp_server.py +21 -0
  39. tests/test_data/packages/dependencies/complex_dep_pkg/hatch_mcp_server.py +18 -0
  40. tests/test_data/packages/dependencies/complex_dep_pkg/mcp_server.py +21 -0
  41. tests/test_data/packages/dependencies/docker_dep_pkg/hatch_mcp_server.py +18 -0
  42. tests/test_data/packages/dependencies/docker_dep_pkg/mcp_server.py +21 -0
  43. tests/test_data/packages/dependencies/mixed_dep_pkg/hatch_mcp_server.py +18 -0
  44. tests/test_data/packages/dependencies/mixed_dep_pkg/mcp_server.py +21 -0
  45. tests/test_data/packages/dependencies/python_dep_pkg/hatch_mcp_server.py +18 -0
  46. tests/test_data/packages/dependencies/python_dep_pkg/mcp_server.py +21 -0
  47. tests/test_data/packages/dependencies/simple_dep_pkg/hatch_mcp_server.py +18 -0
  48. tests/test_data/packages/dependencies/simple_dep_pkg/mcp_server.py +21 -0
  49. tests/test_data/packages/dependencies/system_dep_pkg/hatch_mcp_server.py +18 -0
  50. tests/test_data/packages/dependencies/system_dep_pkg/mcp_server.py +21 -0
  51. tests/test_data/packages/error_scenarios/circular_dep_pkg/hatch_mcp_server.py +18 -0
  52. tests/test_data/packages/error_scenarios/circular_dep_pkg/mcp_server.py +21 -0
  53. tests/test_data/packages/error_scenarios/circular_dep_pkg_b/hatch_mcp_server.py +18 -0
  54. tests/test_data/packages/error_scenarios/circular_dep_pkg_b/mcp_server.py +21 -0
  55. tests/test_data/packages/error_scenarios/invalid_dep_pkg/hatch_mcp_server.py +18 -0
  56. tests/test_data/packages/error_scenarios/invalid_dep_pkg/mcp_server.py +21 -0
  57. tests/test_data/packages/error_scenarios/version_conflict_pkg/hatch_mcp_server.py +18 -0
  58. tests/test_data/packages/error_scenarios/version_conflict_pkg/mcp_server.py +21 -0
  59. tests/test_data/packages/schema_versions/schema_v1_1_0_pkg/main.py +11 -0
  60. tests/test_data/packages/schema_versions/schema_v1_2_0_pkg/main.py +11 -0
  61. tests/test_data/packages/schema_versions/schema_v1_2_1_pkg/hatch_mcp_server.py +18 -0
  62. tests/test_data/packages/schema_versions/schema_v1_2_1_pkg/mcp_server.py +21 -0
  63. tests/test_data_utils.py +472 -0
  64. tests/test_dependency_orchestrator_consent.py +266 -0
  65. tests/test_docker_installer.py +524 -0
  66. tests/test_env_manip.py +991 -0
  67. tests/test_hatch_installer.py +179 -0
  68. tests/test_installer_base.py +221 -0
  69. tests/test_mcp_atomic_operations.py +276 -0
  70. tests/test_mcp_backup_integration.py +308 -0
  71. tests/test_mcp_cli_all_host_specific_args.py +303 -0
  72. tests/test_mcp_cli_backup_management.py +295 -0
  73. tests/test_mcp_cli_direct_management.py +453 -0
  74. tests/test_mcp_cli_discovery_listing.py +582 -0
  75. tests/test_mcp_cli_host_config_integration.py +823 -0
  76. tests/test_mcp_cli_package_management.py +360 -0
  77. tests/test_mcp_cli_partial_updates.py +859 -0
  78. tests/test_mcp_environment_integration.py +520 -0
  79. tests/test_mcp_host_config_backup.py +257 -0
  80. tests/test_mcp_host_configuration_manager.py +331 -0
  81. tests/test_mcp_host_registry_decorator.py +348 -0
  82. tests/test_mcp_pydantic_architecture_v4.py +603 -0
  83. tests/test_mcp_server_config_models.py +242 -0
  84. tests/test_mcp_server_config_type_field.py +221 -0
  85. tests/test_mcp_sync_functionality.py +316 -0
  86. tests/test_mcp_user_feedback_reporting.py +359 -0
  87. tests/test_non_tty_integration.py +281 -0
  88. tests/test_online_package_loader.py +202 -0
  89. tests/test_python_environment_manager.py +882 -0
  90. tests/test_python_installer.py +327 -0
  91. tests/test_registry.py +51 -0
  92. tests/test_registry_retriever.py +250 -0
  93. 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()