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,882 @@
1
+ """Tests for PythonEnvironmentManager.
2
+
3
+ This module contains tests for the Python environment management functionality,
4
+ including conda/mamba environment creation, configuration, and integration.
5
+ """
6
+ import shutil
7
+ import tempfile
8
+ import unittest
9
+ from pathlib import Path
10
+ from unittest.mock import Mock, patch, MagicMock
11
+
12
+ from wobble.decorators import regression_test, integration_test, slow_test
13
+
14
+ from hatch.python_environment_manager import PythonEnvironmentManager, PythonEnvironmentError
15
+
16
+
17
+ class TestPythonEnvironmentManager(unittest.TestCase):
18
+ """Test cases for PythonEnvironmentManager functionality."""
19
+
20
+ def setUp(self):
21
+ """Set up test environment."""
22
+ self.temp_dir = tempfile.mkdtemp()
23
+ self.environments_dir = Path(self.temp_dir) / "envs"
24
+ self.environments_dir.mkdir(exist_ok=True)
25
+
26
+ # Create manager instance for testing
27
+ self.manager = PythonEnvironmentManager(environments_dir=self.environments_dir)
28
+
29
+ # Track environments created during this test for cleanup
30
+ self.created_environments = []
31
+
32
+ def tearDown(self):
33
+ """Clean up test environment."""
34
+ # Clean up any conda/mamba environments created during this test
35
+ if hasattr(self, 'manager') and self.manager.is_available():
36
+ for env_name in self.created_environments:
37
+ try:
38
+ if self.manager.environment_exists(env_name):
39
+ self.manager.remove_python_environment(env_name)
40
+ except Exception:
41
+ pass # Best effort cleanup
42
+
43
+ # Clean up temporary directory
44
+ shutil.rmtree(self.temp_dir, ignore_errors=True)
45
+
46
+ def _track_environment(self, env_name):
47
+ """Track an environment for cleanup in tearDown."""
48
+ if env_name not in self.created_environments:
49
+ self.created_environments.append(env_name)
50
+
51
+ @regression_test
52
+ @patch('hatch.python_environment_manager.PythonEnvironmentManager._conda_env_exists', return_value=True)
53
+ @patch('hatch.python_environment_manager.PythonEnvironmentManager._get_conda_env_name', return_value='hatch_test_env')
54
+ @patch('hatch.python_environment_manager.PythonEnvironmentManager._get_python_executable_path', return_value='C:/fake/env/Scripts/python.exe')
55
+ @patch('hatch.python_environment_manager.PythonEnvironmentManager.get_environment_path', return_value=Path('C:/fake/env'))
56
+ @patch('platform.system', return_value='Windows')
57
+ def test_get_environment_activation_info_windows(self, mock_platform, mock_get_env_path, mock_get_python_exec_path, mock_get_conda_env_name, mock_conda_env_exists):
58
+ """Test get_environment_activation_info returns correct env vars on Windows."""
59
+ env_name = 'test_env'
60
+ manager = PythonEnvironmentManager(environments_dir=Path('C:/fake/envs'))
61
+ env_vars = manager.get_environment_activation_info(env_name)
62
+ self.assertIsInstance(env_vars, dict)
63
+ self.assertEqual(env_vars['CONDA_DEFAULT_ENV'], 'hatch_test_env')
64
+ self.assertEqual(env_vars['CONDA_PREFIX'], str(Path('C:/fake/env')))
65
+ self.assertIn('PATH', env_vars)
66
+ # On Windows, the path separator is ';' and paths are backslash
67
+ # Split PATH and check each expected directory is present as a component
68
+ path_dirs = env_vars['PATH'].split(';')
69
+ self.assertIn('C:\\fake\\env', path_dirs)
70
+ self.assertIn('C:\\fake\\env\\Scripts', path_dirs)
71
+ self.assertIn('C:\\fake\\env\\Library\\bin', path_dirs)
72
+ self.assertEqual(env_vars['PYTHON'], 'C:/fake/env/Scripts/python.exe')
73
+
74
+ @regression_test
75
+ @patch('hatch.python_environment_manager.PythonEnvironmentManager._conda_env_exists', return_value=True)
76
+ @patch('hatch.python_environment_manager.PythonEnvironmentManager._get_conda_env_name', return_value='hatch_test_env')
77
+ @patch('hatch.python_environment_manager.PythonEnvironmentManager._get_python_executable_path', return_value='/fake/env/bin/python')
78
+ @patch('hatch.python_environment_manager.PythonEnvironmentManager.get_environment_path', return_value=Path('/fake/env'))
79
+ @patch('platform.system', return_value='Linux')
80
+ def test_get_environment_activation_info_unix(self, mock_platform, mock_get_env_path, mock_get_python_exec_path, mock_get_conda_env_name, mock_conda_env_exists):
81
+ """Test get_environment_activation_info returns correct env vars on Unix."""
82
+ env_name = 'test_env'
83
+ manager = PythonEnvironmentManager(environments_dir=Path('/fake/envs'))
84
+ env_vars = manager.get_environment_activation_info(env_name)
85
+ self.assertIsInstance(env_vars, dict)
86
+ self.assertEqual(env_vars['CONDA_DEFAULT_ENV'], 'hatch_test_env')
87
+ self.assertEqual(env_vars['CONDA_PREFIX'], str(Path('/fake/env')))
88
+ self.assertIn('PATH', env_vars)
89
+ # On Unix, the path separator is ':' and paths are forward slash, but Path() may normalize to backslash on Windows
90
+ # Accept both possible representations for cross-platform test running
91
+ path_dirs = env_vars['PATH']
92
+ self.assertTrue('/fake/env/bin' in path_dirs or '\\fake\\env\\bin' in path_dirs, f"Expected '/fake/env/bin' or '\\fake\\env\\bin' to be in PATH: {env_vars['PATH']}")
93
+ self.assertEqual(env_vars['PYTHON'], '/fake/env/bin/python')
94
+
95
+ @regression_test
96
+ @patch('hatch.python_environment_manager.PythonEnvironmentManager._conda_env_exists', return_value=False)
97
+ def test_get_environment_activation_info_env_not_exists(self, mock_conda_env_exists):
98
+ """Test get_environment_activation_info returns None if env does not exist."""
99
+ env_name = 'nonexistent_env'
100
+ manager = PythonEnvironmentManager(environments_dir=Path('/fake/envs'))
101
+ env_vars = manager.get_environment_activation_info(env_name)
102
+ self.assertIsNone(env_vars)
103
+
104
+ @regression_test
105
+ @patch('hatch.python_environment_manager.PythonEnvironmentManager._conda_env_exists', return_value=True)
106
+ @patch('hatch.python_environment_manager.PythonEnvironmentManager._get_python_executable_path', return_value=None)
107
+ def test_get_environment_activation_info_no_python(self, mock_get_python_exec_path, mock_conda_env_exists):
108
+ """Test get_environment_activation_info returns None if python executable not found."""
109
+ env_name = 'test_env'
110
+ manager = PythonEnvironmentManager(environments_dir=Path('/fake/envs'))
111
+ env_vars = manager.get_environment_activation_info(env_name)
112
+ self.assertIsNone(env_vars)
113
+
114
+ @regression_test
115
+ def test_init(self):
116
+ """Test PythonEnvironmentManager initialization."""
117
+ self.assertEqual(self.manager.environments_dir, self.environments_dir)
118
+ self.assertIsNotNone(self.manager.logger)
119
+
120
+ @regression_test
121
+ def test_detect_conda_mamba_with_mamba(self):
122
+ """Test conda/mamba detection when mamba is available."""
123
+ with patch.object(PythonEnvironmentManager, "_detect_manager") as mock_detect:
124
+ # mamba found, conda found
125
+ mock_detect.side_effect = lambda manager: "/usr/bin/mamba" if manager == "mamba" else "/usr/bin/conda"
126
+ manager = PythonEnvironmentManager(environments_dir=self.environments_dir)
127
+ self.assertEqual(manager.mamba_executable, "/usr/bin/mamba")
128
+ self.assertEqual(manager.conda_executable, "/usr/bin/conda")
129
+
130
+ @regression_test
131
+ def test_detect_conda_mamba_conda_only(self):
132
+ """Test conda/mamba detection when only conda is available."""
133
+ with patch.object(PythonEnvironmentManager, "_detect_manager") as mock_detect:
134
+ # mamba not found, conda found
135
+ mock_detect.side_effect = lambda manager: None if manager == "mamba" else "/usr/bin/conda"
136
+ manager = PythonEnvironmentManager(environments_dir=self.environments_dir)
137
+ self.assertIsNone(manager.mamba_executable)
138
+ self.assertEqual(manager.conda_executable, "/usr/bin/conda")
139
+
140
+ @regression_test
141
+ def test_detect_conda_mamba_none_available(self):
142
+ """Test conda/mamba detection when neither is available."""
143
+ with patch.object(PythonEnvironmentManager, "_detect_manager", return_value=None):
144
+ manager = PythonEnvironmentManager(environments_dir=self.environments_dir)
145
+ self.assertIsNone(manager.mamba_executable)
146
+ self.assertIsNone(manager.conda_executable)
147
+
148
+ @regression_test
149
+ def test_get_conda_env_name(self):
150
+ """Test conda environment name generation."""
151
+ env_name = "test_env"
152
+ conda_name = self.manager._get_conda_env_name(env_name)
153
+ self.assertEqual(conda_name, "hatch_test_env")
154
+
155
+ @regression_test
156
+ @patch('subprocess.run')
157
+ def test_get_python_executable_path_windows(self, mock_run):
158
+ """Test Python executable path on Windows."""
159
+ with patch('platform.system', return_value='Windows'):
160
+ env_name = "test_env"
161
+
162
+ # Mock conda info command to return environment path
163
+ mock_run.return_value = Mock(
164
+ returncode=0,
165
+ stdout='{"envs": ["/conda/envs/hatch_test_env"]}'
166
+ )
167
+
168
+ python_path = self.manager._get_python_executable_path(env_name)
169
+ expected = Path("/conda/envs/hatch_test_env/python.exe")
170
+ self.assertEqual(python_path, expected)
171
+
172
+ @regression_test
173
+ @patch('subprocess.run')
174
+ def test_get_python_executable_path_unix(self, mock_run):
175
+ """Test Python executable path on Unix/Linux."""
176
+ with patch('platform.system', return_value='Linux'):
177
+ env_name = "test_env"
178
+
179
+ # Mock conda info command to return environment path
180
+ mock_run.return_value = Mock(
181
+ returncode=0,
182
+ stdout='{"envs": ["/conda/envs/hatch_test_env"]}'
183
+ )
184
+
185
+ python_path = self.manager._get_python_executable_path(env_name)
186
+ expected = Path("/conda/envs/hatch_test_env/bin/python")
187
+ self.assertEqual(python_path, expected)
188
+
189
+ @regression_test
190
+ def test_is_available_no_conda(self):
191
+ """Test availability check when conda/mamba is not available."""
192
+ manager = PythonEnvironmentManager(environments_dir=self.environments_dir)
193
+ manager.conda_executable = None
194
+ manager.mamba_executable = None
195
+
196
+ self.assertFalse(manager.is_available())
197
+
198
+ @regression_test
199
+ @patch('subprocess.run')
200
+ def test_is_available_with_conda(self, mock_run):
201
+ """Test availability check when conda is available."""
202
+ self.manager.conda_executable = "/usr/bin/conda"
203
+
204
+ # Mock successful conda info
205
+ mock_run.return_value = Mock(returncode=0, stdout='{"platform": "linux-64"}')
206
+
207
+ self.assertTrue(self.manager.is_available())
208
+
209
+ @regression_test
210
+ def test_get_preferred_executable(self):
211
+ """Test preferred executable selection."""
212
+ # Test mamba preferred over conda
213
+ self.manager.mamba_executable = "/usr/bin/mamba"
214
+ self.manager.conda_executable = "/usr/bin/conda"
215
+ self.assertEqual(self.manager.get_preferred_executable(), "/usr/bin/mamba")
216
+
217
+ # Test conda when mamba not available
218
+ self.manager.mamba_executable = None
219
+ self.assertEqual(self.manager.get_preferred_executable(), "/usr/bin/conda")
220
+
221
+ # Test None when neither available
222
+ self.manager.conda_executable = None
223
+ self.assertIsNone(self.manager.get_preferred_executable())
224
+
225
+ @regression_test
226
+ @patch('shutil.which')
227
+ @patch('subprocess.run')
228
+ def test_create_python_environment_success(self, mock_run, mock_which):
229
+ """Test successful Python environment creation."""
230
+ # Patch mamba detection
231
+ mock_which.side_effect = lambda cmd: "/usr/bin/mamba" if cmd == "mamba" else None
232
+
233
+ # Patch subprocess.run for both validation and creation
234
+ def run_side_effect(cmd, *args, **kwargs):
235
+ if "info" in cmd:
236
+ # Validation call
237
+ return Mock(returncode=0, stdout='{"platform": "win-64"}')
238
+ elif "create" in cmd:
239
+ # Environment creation call
240
+ return Mock(returncode=0, stdout="Environment created")
241
+ else:
242
+ return Mock(returncode=0, stdout="")
243
+ mock_run.side_effect = run_side_effect
244
+
245
+ manager = PythonEnvironmentManager(environments_dir=self.environments_dir)
246
+
247
+ # Mock environment existence check
248
+ with patch.object(manager, '_conda_env_exists', return_value=False):
249
+ result = manager.create_python_environment("test_env", python_version="3.11")
250
+ self.assertTrue(result)
251
+ mock_run.assert_called()
252
+
253
+ @regression_test
254
+ def test_create_python_environment_no_conda(self):
255
+ """Test Python environment creation when conda/mamba is not available."""
256
+ self.manager.conda_executable = None
257
+ self.manager.mamba_executable = None
258
+
259
+ with self.assertRaises(PythonEnvironmentError):
260
+ self.manager.create_python_environment("test_env")
261
+
262
+ @regression_test
263
+ @patch('shutil.which')
264
+ @patch('subprocess.run')
265
+ def test_create_python_environment_already_exists(self, mock_run, mock_which):
266
+ """Test Python environment creation when environment already exists."""
267
+ # Patch mamba detection
268
+ mock_which.side_effect = lambda cmd: "/usr/bin/mamba" if cmd == "mamba" else None
269
+
270
+ # Patch subprocess.run for both validation and creation
271
+ def run_side_effect(cmd, *args, **kwargs):
272
+ if "info" in cmd:
273
+ # Validation call
274
+ return Mock(returncode=0, stdout='{"platform": "win-64"}')
275
+ elif "create" in cmd:
276
+ # Environment creation call
277
+ return Mock(returncode=0, stdout="Environment created")
278
+ else:
279
+ return Mock(returncode=0, stdout="")
280
+ mock_run.side_effect = run_side_effect
281
+
282
+ # Mock environment already exists
283
+ with patch.object(self.manager, '_conda_env_exists', return_value=True):
284
+ result = self.manager.create_python_environment("test_env")
285
+ self.assertTrue(result)
286
+ # Ensure 'create' was not called, but 'info' was
287
+ create_calls = [call for call in mock_run.call_args_list if "create" in call[0][0]]
288
+ self.assertEqual(len(create_calls), 0)
289
+
290
+ @regression_test
291
+ @patch('subprocess.run')
292
+ def test_conda_env_exists(self, mock_run):
293
+ """Test conda environment existence check."""
294
+ env_name = "test_env"
295
+
296
+ # Mock conda env list to return the environment
297
+ mock_run.return_value = Mock(
298
+ returncode=0,
299
+ stdout='{"envs": ["/conda/envs/hatch_test_env", "/conda/envs/other_env"]}'
300
+ )
301
+
302
+ self.assertTrue(self.manager._conda_env_exists(env_name))
303
+
304
+ @regression_test
305
+ @patch('subprocess.run')
306
+ def test_conda_env_not_exists(self, mock_run):
307
+ """Test conda environment existence check when environment doesn't exist."""
308
+ env_name = "nonexistent_env"
309
+
310
+ # Mock conda env list to not return the environment
311
+ mock_run.return_value = Mock(
312
+ returncode=0,
313
+ stdout='{"envs": ["/conda/envs/other_env"]}'
314
+ )
315
+
316
+ self.assertFalse(self.manager._conda_env_exists(env_name))
317
+
318
+ @regression_test
319
+ @patch('subprocess.run')
320
+ def test_get_python_executable_exists(self, mock_run):
321
+ """Test getting Python executable when environment exists."""
322
+ env_name = "test_env"
323
+
324
+ # Mock conda env list to show environment exists
325
+ def run_side_effect(cmd, *args, **kwargs):
326
+ if "env" in cmd and "list" in cmd:
327
+ return Mock(returncode=0, stdout='{"envs": ["/conda/envs/hatch_test_env"]}')
328
+ elif "info" in cmd and "--envs" in cmd:
329
+ return Mock(returncode=0, stdout='{"envs": ["/conda/envs/hatch_test_env"]}')
330
+ else:
331
+ return Mock(returncode=0, stdout='{}')
332
+
333
+ mock_run.side_effect = run_side_effect
334
+
335
+ # Mock that the file exists
336
+ with patch('pathlib.Path.exists', return_value=True):
337
+ result = self.manager.get_python_executable(env_name)
338
+ import platform
339
+ from pathlib import Path as _Path
340
+ if platform.system() == "Windows":
341
+ expected = str(_Path("\\conda\\envs\\hatch_test_env\\python.exe"))
342
+ else:
343
+ expected = str(_Path("/conda/envs/hatch_test_env/bin/python"))
344
+ self.assertEqual(result, expected)
345
+
346
+ @regression_test
347
+ def test_get_python_executable_not_exists(self):
348
+ """Test getting Python executable when environment doesn't exist."""
349
+ env_name = "nonexistent_env"
350
+
351
+ with patch.object(self.manager, '_conda_env_exists', return_value=False):
352
+ result = self.manager.get_python_executable(env_name)
353
+ self.assertIsNone(result)
354
+
355
+
356
+ class TestPythonEnvironmentManagerIntegration(unittest.TestCase):
357
+ """Integration test cases for PythonEnvironmentManager with real conda/mamba operations.
358
+
359
+ These tests require conda or mamba to be installed on the system and will create
360
+ real conda environments for testing. They are more comprehensive but slower than
361
+ the mocked unit tests.
362
+ """
363
+
364
+ @classmethod
365
+ def setUpClass(cls):
366
+ """Set up class-level test environment."""
367
+ cls.temp_dir = tempfile.mkdtemp()
368
+ cls.environments_dir = Path(cls.temp_dir) / "envs"
369
+ cls.environments_dir.mkdir(exist_ok=True)
370
+
371
+ # Create manager instance for integration testing
372
+ cls.manager = PythonEnvironmentManager(environments_dir=cls.environments_dir)
373
+
374
+ # Track all environments created during integration tests
375
+ cls.all_created_environments = set()
376
+
377
+ # Skip all tests if conda/mamba is not available
378
+ if not cls.manager.is_available():
379
+ raise unittest.SkipTest("Conda/mamba not available for integration tests")
380
+
381
+ def setUp(self):
382
+ """Set up individual test."""
383
+ # Track environments created during this specific test
384
+ self.test_environments = []
385
+
386
+ def tearDown(self):
387
+ """Clean up individual test."""
388
+ # Clean up environments created during this specific test
389
+ for env_name in self.test_environments:
390
+ try:
391
+ if self.manager.environment_exists(env_name):
392
+ self.manager.remove_python_environment(env_name)
393
+ self.all_created_environments.discard(env_name)
394
+ except Exception:
395
+ pass # Best effort cleanup
396
+
397
+ def _track_environment(self, env_name):
398
+ """Track an environment for cleanup."""
399
+ if env_name not in self.test_environments:
400
+ self.test_environments.append(env_name)
401
+ self.all_created_environments.add(env_name)
402
+
403
+ @classmethod
404
+ def tearDownClass(cls):
405
+ """Clean up class-level test environment."""
406
+ # Clean up any remaining test environments
407
+ try:
408
+ # Clean up tracked environments
409
+ for env_name in list(cls.all_created_environments):
410
+ if cls.manager.environment_exists(env_name):
411
+ cls.manager.remove_python_environment(env_name)
412
+
413
+ # Clean up known test environment patterns (fallback)
414
+ known_patterns = [
415
+ "test_integration_env", "test_python_311", "test_python_312", "test_diagnostics_env",
416
+ "test_env_1", "test_env_2", "test_env_3", "test_env_4", "test_env_5",
417
+ "test_python_39", "test_python_310", "test_python_312", "test_cache_env1", "test_cache_env2"
418
+ ]
419
+ for env_name in known_patterns:
420
+ if cls.manager.environment_exists(env_name):
421
+ cls.manager.remove_python_environment(env_name)
422
+ except Exception:
423
+ pass # Best effort cleanup
424
+
425
+ shutil.rmtree(cls.temp_dir, ignore_errors=True)
426
+
427
+ @integration_test(scope="system")
428
+ @slow_test
429
+ def test_conda_mamba_detection_real(self):
430
+ """Test real conda/mamba detection on the system."""
431
+ manager_info = self.manager.get_manager_info()
432
+
433
+ # At least one should be available since we skip tests if neither is available
434
+ self.assertTrue(manager_info["is_available"])
435
+ self.assertTrue(
436
+ manager_info["conda_executable"] is not None or
437
+ manager_info["mamba_executable"] is not None
438
+ )
439
+
440
+ # Preferred manager should be set
441
+ self.assertIsNotNone(manager_info["preferred_manager"])
442
+
443
+ # Platform and Python version should be populated
444
+ self.assertIsNotNone(manager_info["platform"])
445
+ self.assertIsNotNone(manager_info["python_version"])
446
+
447
+ @integration_test(scope="system")
448
+ @slow_test
449
+ def test_manager_diagnostics_real(self):
450
+ """Test real manager diagnostics."""
451
+ diagnostics = self.manager.get_manager_diagnostics()
452
+
453
+ # Should have basic information
454
+ self.assertIn("any_manager_available", diagnostics)
455
+ self.assertTrue(diagnostics["any_manager_available"])
456
+ self.assertIn("platform", diagnostics)
457
+ self.assertIn("python_version", diagnostics)
458
+ self.assertIn("environments_dir", diagnostics)
459
+
460
+ # Should test actual executables
461
+ if diagnostics["conda_executable"]:
462
+ self.assertIn("conda_works", diagnostics)
463
+ self.assertIn("conda_version", diagnostics)
464
+
465
+ if diagnostics["mamba_executable"]:
466
+ self.assertIn("mamba_works", diagnostics)
467
+ self.assertIn("mamba_version", diagnostics)
468
+
469
+ @integration_test(scope="system")
470
+ @slow_test
471
+ def test_create_and_remove_python_environment_real(self):
472
+ """Test real Python environment creation and removal."""
473
+ env_name = "test_integration_env"
474
+ self._track_environment(env_name)
475
+
476
+ # Ensure environment doesn't exist initially
477
+ if self.manager.environment_exists(env_name):
478
+ self.manager.remove_python_environment(env_name)
479
+
480
+ # Create environment
481
+ result = self.manager.create_python_environment(env_name)
482
+ self.assertTrue(result, "Failed to create Python environment")
483
+
484
+ # Verify environment exists
485
+ self.assertTrue(self.manager.environment_exists(env_name))
486
+
487
+ # Verify Python executable is available
488
+ python_exec = self.manager.get_python_executable(env_name)
489
+ self.assertIsNotNone(python_exec, "Python executable not found")
490
+ self.assertTrue(Path(python_exec).exists(), f"Python executable doesn't exist: {python_exec}")
491
+
492
+ # Get environment info
493
+ env_info = self.manager.get_environment_info(env_name)
494
+ self.assertIsNotNone(env_info)
495
+ self.assertEqual(env_info["environment_name"], env_name)
496
+ self.assertIsNotNone(env_info["conda_env_name"])
497
+ self.assertIsNotNone(env_info["python_executable"])
498
+
499
+ # Remove environment
500
+ result = self.manager.remove_python_environment(env_name)
501
+ self.assertTrue(result, "Failed to remove Python environment")
502
+
503
+ # Verify environment no longer exists
504
+ self.assertFalse(self.manager.environment_exists(env_name))
505
+
506
+ @integration_test(scope="system")
507
+ @slow_test
508
+ def test_create_python_environment_with_version_real(self):
509
+ """Test real Python environment creation with specific version."""
510
+ env_name = "test_python_311"
511
+ self._track_environment(env_name)
512
+ python_version = "3.11"
513
+
514
+ # Ensure environment doesn't exist initially
515
+ if self.manager.environment_exists(env_name):
516
+ self.manager.remove_python_environment(env_name)
517
+
518
+ # Create environment with specific Python version
519
+ result = self.manager.create_python_environment(env_name, python_version=python_version)
520
+ self.assertTrue(result, f"Failed to create Python {python_version} environment")
521
+
522
+ # Verify environment exists
523
+ self.assertTrue(self.manager.environment_exists(env_name))
524
+
525
+ # Verify Python version
526
+ actual_version = self.manager.get_python_version(env_name)
527
+ self.assertIsNotNone(actual_version)
528
+ self.assertTrue(actual_version.startswith("3.11"), f"Expected Python 3.11.x, got {actual_version}")
529
+
530
+ # Get comprehensive environment info
531
+ env_info = self.manager.get_environment_info(env_name)
532
+ self.assertIsNotNone(env_info)
533
+ self.assertTrue(env_info["python_version"].startswith("3.11"), f"Expected Python 3.11.x, got {env_info['python_version']}")
534
+
535
+ # Cleanup
536
+ self.manager.remove_python_environment(env_name)
537
+
538
+ @integration_test(scope="system")
539
+ @slow_test
540
+ def test_environment_diagnostics_real(self):
541
+ """Test real environment diagnostics."""
542
+ env_name = "test_diagnostics_env"
543
+
544
+ # Ensure environment doesn't exist initially
545
+ if self.manager.environment_exists(env_name):
546
+ self.manager.remove_python_environment(env_name)
547
+
548
+ # Test diagnostics for non-existent environment
549
+ diagnostics = self.manager.get_environment_diagnostics(env_name)
550
+ self.assertFalse(diagnostics["exists"])
551
+ self.assertTrue(diagnostics["conda_available"])
552
+
553
+ # Create environment
554
+ self.manager.create_python_environment(env_name)
555
+
556
+ # Test diagnostics for existing environment
557
+ diagnostics = self.manager.get_environment_diagnostics(env_name)
558
+ self.assertTrue(diagnostics["exists"])
559
+ self.assertIsNotNone(diagnostics["python_executable"])
560
+ self.assertTrue(diagnostics["python_accessible"])
561
+ self.assertIsNotNone(diagnostics["python_version"])
562
+ self.assertTrue(diagnostics["python_version_accessible"])
563
+ self.assertTrue(diagnostics["python_executable_works"])
564
+ self.assertIsNotNone(diagnostics["environment_path"])
565
+ self.assertTrue(diagnostics["environment_path_exists"])
566
+
567
+ # Cleanup
568
+ self.manager.remove_python_environment(env_name)
569
+
570
+ @integration_test(scope="system")
571
+ @slow_test
572
+ def test_force_recreation_real(self):
573
+ """Test force recreation of existing environment."""
574
+ env_name = "test_integration_env"
575
+
576
+ # Ensure environment doesn't exist initially
577
+ if self.manager.environment_exists(env_name):
578
+ self.manager.remove_python_environment(env_name)
579
+
580
+ # Create environment
581
+ result1 = self.manager.create_python_environment(env_name)
582
+ self.assertTrue(result1)
583
+
584
+ # Get initial Python executable
585
+ python_exec1 = self.manager.get_python_executable(env_name)
586
+ self.assertIsNotNone(python_exec1)
587
+
588
+ # Try to create again without force (should succeed but not recreate)
589
+ result2 = self.manager.create_python_environment(env_name, force=False)
590
+ self.assertTrue(result2)
591
+
592
+ # Try to create again with force (should recreate)
593
+ result3 = self.manager.create_python_environment(env_name, force=True)
594
+ self.assertTrue(result3)
595
+
596
+ # Verify environment still exists and works
597
+ self.assertTrue(self.manager.environment_exists(env_name))
598
+ python_exec3 = self.manager.get_python_executable(env_name)
599
+ self.assertIsNotNone(python_exec3)
600
+
601
+ # Cleanup
602
+ self.manager.remove_python_environment(env_name)
603
+
604
+ @integration_test(scope="system")
605
+ @slow_test
606
+ def test_list_environments_real(self):
607
+ """Test listing environments with real conda environments."""
608
+ test_envs = ["test_env_1", "test_env_2"]
609
+ final_names = ["hatch_test_env_1", "hatch_test_env_2"]
610
+
611
+ # Track environments for cleanup
612
+ for env_name in test_envs:
613
+ self._track_environment(env_name)
614
+
615
+ # Clean up any existing test environments
616
+ for env_name in test_envs:
617
+ if self.manager.environment_exists(env_name):
618
+ self.manager.remove_python_environment(env_name)
619
+
620
+ # Create test environments
621
+ for env_name in test_envs:
622
+ result = self.manager.create_python_environment(env_name)
623
+ self.assertTrue(result, f"Failed to create {env_name}")
624
+
625
+ # List environments
626
+ env_list = self.manager.list_environments()
627
+
628
+ # Should include our test environments
629
+ for env_name in final_names:
630
+ self.assertIn(env_name, env_list, f"{env_name} not found in environment list")
631
+
632
+ # Cleanup
633
+ for env_name in final_names:
634
+ self.manager.remove_python_environment(env_name)
635
+
636
+ @integration_test(scope="system")
637
+ @slow_test
638
+ @unittest.skipIf(
639
+ not (Path("/usr/bin/python3.12").exists() or Path("/usr/bin/python3.9").exists()),
640
+ "Multiple Python versions not available for testing"
641
+ )
642
+ def test_multiple_python_versions_real(self):
643
+ """Test creating environments with multiple Python versions."""
644
+ test_cases = [
645
+ ("test_python_39", "3.9"),
646
+ ("test_python_312", "3.12")
647
+ ]
648
+
649
+ created_envs = []
650
+
651
+ try:
652
+ for env_name, python_version in test_cases:
653
+ # Skip if this Python version is not available
654
+ try:
655
+ result = self.manager.create_python_environment(env_name, python_version=python_version)
656
+ if result:
657
+ created_envs.append(env_name)
658
+
659
+ # Verify Python version
660
+ actual_version = self.manager.get_python_version(env_name)
661
+ self.assertIsNotNone(actual_version)
662
+ self.assertTrue(
663
+ actual_version.startswith(python_version),
664
+ f"Expected Python {python_version}.x, got {actual_version}"
665
+ )
666
+ except Exception as e:
667
+ # Log but don't fail test if specific Python version is not available
668
+ print(f"Skipping Python {python_version} test: {e}")
669
+
670
+ finally:
671
+ # Cleanup
672
+ for env_name in created_envs:
673
+ try:
674
+ self.manager.remove_python_environment(env_name)
675
+ except Exception:
676
+ pass # Best effort cleanup
677
+
678
+ @integration_test(scope="system")
679
+ @slow_test
680
+ def test_error_handling_real(self):
681
+ """Test error handling with real operations."""
682
+ # Test removing non-existent environment
683
+ result = self.manager.remove_python_environment("nonexistent_env")
684
+ self.assertTrue(result) # Removing non existent environment returns True because it does nothing
685
+
686
+ # Test getting info for non-existent environment
687
+ info = self.manager.get_environment_info("nonexistent_env")
688
+ self.assertIsNone(info)
689
+
690
+ # Test getting Python executable for non-existent environment
691
+ python_exec = self.manager.get_python_executable("nonexistent_env")
692
+ self.assertIsNone(python_exec)
693
+
694
+ # Test diagnostics for non-existent environment
695
+ diagnostics = self.manager.get_environment_diagnostics("nonexistent_env")
696
+ self.assertFalse(diagnostics["exists"])
697
+
698
+
699
+ class TestPythonEnvironmentManagerEnhancedFeatures(unittest.TestCase):
700
+ """Test cases for enhanced features like shell launching and advanced diagnostics."""
701
+
702
+ def setUp(self):
703
+ """Set up test environment."""
704
+ self.temp_dir = tempfile.mkdtemp()
705
+ self.environments_dir = Path(self.temp_dir) / "envs"
706
+ self.environments_dir.mkdir(exist_ok=True)
707
+
708
+ # Create manager instance for testing
709
+ self.manager = PythonEnvironmentManager(environments_dir=self.environments_dir)
710
+
711
+ # Track environments created during this test for cleanup
712
+ self.created_environments = []
713
+
714
+ def tearDown(self):
715
+ """Clean up test environment."""
716
+ # Clean up any conda/mamba environments created during this test
717
+ if hasattr(self, 'manager') and self.manager.is_available():
718
+ for env_name in self.created_environments:
719
+ try:
720
+ if self.manager.environment_exists(env_name):
721
+ self.manager.remove_python_environment(env_name)
722
+ except Exception:
723
+ pass # Best effort cleanup
724
+
725
+ # Clean up temporary directory
726
+ shutil.rmtree(self.temp_dir, ignore_errors=True)
727
+
728
+ def _track_environment(self, env_name):
729
+ """Track an environment for cleanup in tearDown."""
730
+ if env_name not in self.created_environments:
731
+ self.created_environments.append(env_name)
732
+
733
+ @regression_test
734
+ @patch('subprocess.run')
735
+ def test_launch_shell_with_command(self, mock_run):
736
+ """Test launching shell with specific command."""
737
+ env_name = "test_shell_env"
738
+ cmd = "print('Hello from Python')"
739
+
740
+ # Mock environment existence and Python executable
741
+ with patch.object(self.manager, 'environment_exists', return_value=True), \
742
+ patch.object(self.manager, 'get_python_executable', return_value="/path/to/python"):
743
+
744
+ mock_run.return_value = Mock(returncode=0)
745
+
746
+ result = self.manager.launch_shell(env_name, cmd)
747
+ self.assertTrue(result)
748
+
749
+ # Verify subprocess was called with correct arguments
750
+ mock_run.assert_called_once()
751
+ call_args = mock_run.call_args[0][0]
752
+ self.assertIn("/path/to/python", call_args)
753
+ self.assertIn("-c", call_args)
754
+ self.assertIn(cmd, call_args)
755
+
756
+ @regression_test
757
+ @patch('subprocess.run')
758
+ @patch('platform.system')
759
+ def test_launch_shell_interactive_windows(self, mock_platform, mock_run):
760
+ """Test launching interactive shell on Windows."""
761
+ mock_platform.return_value = "Windows"
762
+ env_name = "test_shell_env"
763
+
764
+ # Mock environment existence and Python executable
765
+ with patch.object(self.manager, 'environment_exists', return_value=True), \
766
+ patch.object(self.manager, 'get_python_executable', return_value="/path/to/python"):
767
+
768
+ mock_run.return_value = Mock(returncode=0)
769
+
770
+ result = self.manager.launch_shell(env_name)
771
+ self.assertTrue(result)
772
+
773
+ # Verify subprocess was called for Windows
774
+ mock_run.assert_called_once()
775
+ call_args = mock_run.call_args[0][0]
776
+ self.assertIn("cmd", call_args)
777
+ self.assertIn("/c", call_args)
778
+
779
+ @regression_test
780
+ @patch('subprocess.run')
781
+ @patch('platform.system')
782
+ def test_launch_shell_interactive_unix(self, mock_platform, mock_run):
783
+ """Test launching interactive shell on Unix."""
784
+ mock_platform.return_value = "Linux"
785
+ env_name = "test_shell_env"
786
+
787
+ # Mock environment existence and Python executable
788
+ with patch.object(self.manager, 'environment_exists', return_value=True), \
789
+ patch.object(self.manager, 'get_python_executable', return_value="/path/to/python"):
790
+
791
+ mock_run.return_value = Mock(returncode=0)
792
+
793
+ result = self.manager.launch_shell(env_name)
794
+ self.assertTrue(result)
795
+
796
+ # Verify subprocess was called with Python executable directly
797
+ mock_run.assert_called_once()
798
+ call_args = mock_run.call_args[0][0]
799
+ self.assertEqual(call_args, ["/path/to/python"])
800
+
801
+ @regression_test
802
+ def test_launch_shell_nonexistent_environment(self):
803
+ """Test launching shell for non-existent environment."""
804
+ env_name = "nonexistent_env"
805
+
806
+ with patch.object(self.manager, 'environment_exists', return_value=False):
807
+ result = self.manager.launch_shell(env_name)
808
+ self.assertFalse(result)
809
+
810
+ @regression_test
811
+ def test_launch_shell_no_python_executable(self):
812
+ """Test launching shell when Python executable is not found."""
813
+ env_name = "test_shell_env"
814
+
815
+ with patch.object(self.manager, 'environment_exists', return_value=True), \
816
+ patch.object(self.manager, 'get_python_executable', return_value=None):
817
+
818
+ result = self.manager.launch_shell(env_name)
819
+ self.assertFalse(result)
820
+
821
+ @regression_test
822
+ def test_get_manager_info_structure(self):
823
+ """Test manager info structure and content."""
824
+ info = self.manager.get_manager_info()
825
+
826
+ # Verify required fields are present
827
+ required_fields = [
828
+ "conda_executable", "mamba_executable", "preferred_manager",
829
+ "is_available", "platform", "python_version"
830
+ ]
831
+
832
+ for field in required_fields:
833
+ self.assertIn(field, info, f"Missing required field: {field}")
834
+
835
+ # Verify data types
836
+ self.assertIsInstance(info["is_available"], bool)
837
+ self.assertIsInstance(info["platform"], str)
838
+ self.assertIsInstance(info["python_version"], str)
839
+
840
+ @regression_test
841
+ def test_environment_diagnostics_structure(self):
842
+ """Test environment diagnostics structure."""
843
+ env_name = "test_diagnostics"
844
+ diagnostics = self.manager.get_environment_diagnostics(env_name)
845
+
846
+ # Verify required fields are present
847
+ required_fields = [
848
+ "environment_name", "conda_env_name", "exists", "conda_available",
849
+ "manager_executable", "platform"
850
+ ]
851
+
852
+ for field in required_fields:
853
+ self.assertIn(field, diagnostics, f"Missing required field: {field}")
854
+
855
+ # Verify basic structure
856
+ self.assertEqual(diagnostics["environment_name"], env_name)
857
+ self.assertEqual(diagnostics["conda_env_name"], f"hatch_{env_name}")
858
+ self.assertIsInstance(diagnostics["exists"], bool)
859
+ self.assertIsInstance(diagnostics["conda_available"], bool)
860
+
861
+ @regression_test
862
+ def test_manager_diagnostics_structure(self):
863
+ """Test manager diagnostics structure."""
864
+ diagnostics = self.manager.get_manager_diagnostics()
865
+
866
+ # Verify required fields are present
867
+ required_fields = [
868
+ "conda_executable", "mamba_executable", "conda_available", "mamba_available",
869
+ "any_manager_available", "preferred_manager", "platform", "python_version",
870
+ "environments_dir"
871
+ ]
872
+
873
+ for field in required_fields:
874
+ self.assertIn(field, diagnostics, f"Missing required field: {field}")
875
+
876
+ # Verify data types
877
+ self.assertIsInstance(diagnostics["conda_available"], bool)
878
+ self.assertIsInstance(diagnostics["mamba_available"], bool)
879
+ self.assertIsInstance(diagnostics["any_manager_available"], bool)
880
+ self.assertIsInstance(diagnostics["platform"], str)
881
+ self.assertIsInstance(diagnostics["python_version"], str)
882
+ self.assertIsInstance(diagnostics["environments_dir"], str)