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,25 @@
1
+ """Installer framework for Hatch dependency management.
2
+
3
+ This package provides a robust, extensible installer interface and concrete
4
+ implementations for different dependency types including Hatch packages,
5
+ Python packages, system packages, and Docker containers.
6
+ """
7
+
8
+ from hatch.installers.installer_base import DependencyInstaller, InstallationError, InstallationContext
9
+ from hatch.installers.hatch_installer import HatchInstaller
10
+ from hatch.installers.python_installer import PythonInstaller
11
+ from hatch.installers.system_installer import SystemInstaller
12
+ from hatch.installers.docker_installer import DockerInstaller
13
+ from hatch.installers.registry import InstallerRegistry, installer_registry
14
+
15
+ __all__ = [
16
+ "DependencyInstaller",
17
+ "InstallationError",
18
+ "InstallationContext",
19
+ #"HatchInstaller", # Not necessary to expose directly, the registry will handle it
20
+ #"PythonInstaller", # Not necessary to expose directly, the registry will handle it
21
+ #"SystemInstaller", # Not necessary to expose directly, the registry will handle it
22
+ #"DockerInstaller", # Not necessary to expose directly, the registry will handle it
23
+ "InstallerRegistry",
24
+ "installer_registry"
25
+ ]
@@ -0,0 +1,636 @@
1
+ """Dependency installation orchestrator for coordinating package installation.
2
+
3
+ This module provides centralized orchestration for all dependency installation
4
+ across different dependency types, with centralized user consent management
5
+ and delegation to specific installers.
6
+ """
7
+
8
+ import json
9
+ import logging
10
+ import datetime
11
+ import sys
12
+ import os
13
+ from pathlib import Path
14
+ from typing import Dict, List, Optional, Any, Tuple
15
+
16
+ from hatch_validator.package.package_service import PackageService
17
+ from hatch_validator.registry.registry_service import RegistryService
18
+ from hatch_validator.utils.hatch_dependency_graph import HatchDependencyGraphBuilder
19
+ from hatch_validator.utils.version_utils import VersionConstraintValidator, VersionConstraintError
20
+ from hatch_validator.core.validation_context import ValidationContext
21
+
22
+ from hatch.package_loader import HatchPackageLoader
23
+
24
+
25
+ # Mandatory to insure the installers are registered in the singleton `installer_registry` correctly at import time
26
+ from hatch.installers.hatch_installer import HatchInstaller
27
+ from hatch.installers.python_installer import PythonInstaller
28
+ from hatch.installers.system_installer import SystemInstaller
29
+ from hatch.installers.docker_installer import DockerInstaller
30
+
31
+ from hatch.installers.registry import installer_registry
32
+ from hatch.installers.installer_base import InstallationError
33
+ from hatch.installers.installation_context import InstallationContext, InstallationStatus
34
+
35
+
36
+ class DependencyInstallationError(Exception):
37
+ """Exception raised for dependency installation-related errors."""
38
+ pass
39
+
40
+
41
+ class DependencyInstallerOrchestrator:
42
+ """Orchestrates dependency installation across all supported dependency types.
43
+
44
+ This class coordinates the installation of dependencies by:
45
+ 1. Resolving all dependencies for a given package using the validator
46
+ 2. Aggregating installation plans across all dependency types
47
+ 3. Managing centralized user consent
48
+ 4. Delegating to appropriate installers via the registry
49
+ 5. Handling installation order and error recovery
50
+
51
+ The orchestrator strictly uses PackageService for all metadata access to ensure
52
+ compatibility across different package schema versions.
53
+ """
54
+
55
+ def __init__(self,
56
+ package_loader: HatchPackageLoader,
57
+ registry_service: RegistryService,
58
+ registry_data: Dict[str, Any]):
59
+ """Initialize the dependency installation orchestrator.
60
+
61
+ Args:
62
+ package_loader (HatchPackageLoader): Package loader for file operations.
63
+ registry_service (RegistryService): Service for registry operations.
64
+ registry_data (Dict[str, Any]): Registry data for dependency resolution.
65
+ """
66
+ self.logger = logging.getLogger("hatch.dependency_orchestrator")
67
+ self.logger.setLevel(logging.INFO)
68
+ self.package_loader = package_loader
69
+ self.registry_service = registry_service
70
+ self.registry_data = registry_data
71
+
72
+ # Python executable configuration for context
73
+ self._python_env_vars = Optional[Dict[str, str]] # Environment variables for Python execution
74
+
75
+ # These will be set during package resolution
76
+ self.package_service: Optional[PackageService] = None
77
+ self.dependency_graph_builder: Optional[HatchDependencyGraphBuilder] = None
78
+ self._resolved_package_path: Optional[Path] = None
79
+ self._resolved_package_type: Optional[str] = None
80
+ self._resolved_package_location: Optional[str] = None
81
+
82
+ def set_python_env_vars(self, python_env_vars: Dict[str, str]) -> None:
83
+ """Set the environment variables for the Python executable.
84
+
85
+ Args:
86
+ python_env_vars (Dict[str, str]): Environment variables to set for Python execution.
87
+ """
88
+ self._python_env_vars = python_env_vars
89
+
90
+ def get_python_env_vars(self) -> Optional[Dict[str, str]]:
91
+ """Get the configured environment variables for the Python executable.
92
+
93
+ Returns:
94
+ Dict[str, str]: Environment variables for Python execution, None if not configured.
95
+ """
96
+ return self._python_env_vars
97
+
98
+ def install_single_dep(self, dep: Dict[str, Any], context: InstallationContext) -> Dict[str, Any]:
99
+ """Install a single dependency into the specified environment context.
100
+
101
+ This method installs a single dependency using the appropriate installer from the registry.
102
+ It extracts the core installation logic from _execute_install_plan for reuse in other contexts.
103
+ This method operates with auto_approve=True and does not require user consent.
104
+
105
+ Args:
106
+ dep (Dict[str, Any]): Dependency dictionary following the schema for the dependency type.
107
+ For Python dependencies, should include: name, version_constraint, package_manager.
108
+ Example: {"name": "numpy", "version_constraint": "*", "package_manager": "pip", "type": "python"}
109
+ context (InstallationContext): Installation context with environment path and configuration.
110
+
111
+ Returns:
112
+ Dict[str, Any]: Installed package information containing:
113
+ - name: Package name
114
+ - version: Installed version
115
+ - type: Dependency type
116
+ - source: Package source URI
117
+
118
+ Raises:
119
+ DependencyInstallationError: If installation fails or dependency type is not supported.
120
+ """
121
+ # Ensure dependency has type information
122
+ dep_type = dep.get("type")
123
+ if not dep_type:
124
+ raise DependencyInstallationError(f"Dependency missing 'type' field: {dep}")
125
+
126
+ # Check if installer is registered for this dependency type
127
+ if not installer_registry.is_registered(dep_type):
128
+ raise DependencyInstallationError(f"No installer registered for dependency type: {dep_type}")
129
+
130
+ installer = installer_registry.get_installer(dep_type)
131
+
132
+ try:
133
+ self.logger.info(f"Installing {dep_type} dependency: {dep['name']}")
134
+ self.logger.debug(f"Dependency details: {dep}")
135
+ result = installer.install(dep, context)
136
+ if result.status == InstallationStatus.COMPLETED:
137
+ installed_package = {
138
+ "name": dep["name"],
139
+ "version": dep.get("resolved_version", dep.get("version")),
140
+ "type": dep_type,
141
+ "source": dep.get("uri", "unknown")
142
+ }
143
+ self.logger.info(f"Successfully installed {dep_type} dependency: {dep['name']}")
144
+ return installed_package
145
+ else:
146
+ raise DependencyInstallationError(f"Failed to install {dep['name']}: {result.error_message}")
147
+
148
+ except InstallationError as e:
149
+ self.logger.error(f"Installation error for {dep_type} dependency {dep['name']}: {e.error_code}\n{e.message}")
150
+ raise DependencyInstallationError(f"Installation error for {dep['name']}: {e}") from e
151
+
152
+ except Exception as e:
153
+ self.logger.error(f"Error installing {dep_type} dependency {dep['name']}: {e}")
154
+ raise DependencyInstallationError(f"Error installing {dep['name']}: {e}") from e
155
+
156
+ def install_dependencies(self,
157
+ package_path_or_name: str,
158
+ env_path: Path,
159
+ env_name: str,
160
+ existing_packages: Dict[str, str],
161
+ version_constraint: Optional[str] = None,
162
+ force_download: bool = False,
163
+ auto_approve: bool = False) -> Tuple[bool, List[Dict[str, Any]]]:
164
+ """Install all dependencies for a package with centralized consent management.
165
+
166
+ This method orchestrates the complete dependency installation process by
167
+ leveraging existing validator components and the installer registry. It handles
168
+ all dependency types (hatch, python, system, docker) and provides centralized
169
+ user consent management.
170
+
171
+ Args:
172
+ package_path_or_name (str): Path to local package or name of remote package.
173
+ env_path (Path): Path to the environment directory.
174
+ env_name (str): Name of the environment.
175
+ existing_packages (Dict[str, str]): Currently installed packages {name: version}.
176
+ version_constraint (str, optional): Version constraint for remote packages. Defaults to None.
177
+ force_download (bool, optional): Force download even if package is cached. Defaults to False.
178
+ auto_approve (bool, optional): Skip user consent prompt for automation. Defaults to False.
179
+
180
+ Returns:
181
+ Tuple[bool, List[Dict[str, Any]]]: Success status and list of installed packages.
182
+
183
+ Raises:
184
+ DependencyInstallationError: If installation fails at any stage.
185
+ """
186
+ try:
187
+ # Step 1: Resolve package and load metadata using PackageService
188
+ self._resolve_and_load_package(package_path_or_name, version_constraint, force_download)
189
+
190
+ # Step 2: Get all dependencies organized by type
191
+ dependencies_by_type = self._get_all_dependencies()
192
+
193
+ # Step 3: Filter for missing dependencies by type and track satisfied ones
194
+ missing_dependencies_by_type, satisfied_dependencies_by_type = self._filter_missing_dependencies_by_type(dependencies_by_type, existing_packages)
195
+
196
+ # Step 4: Aggregate installation plan
197
+ install_plan = self._aggregate_install_plan(missing_dependencies_by_type, satisfied_dependencies_by_type)
198
+
199
+ # Step 5: Print installation summary for user review
200
+ self._print_installation_summary(install_plan)
201
+
202
+ # Step 6: Request user consent
203
+ if not auto_approve:
204
+ if not self._request_user_consent(install_plan):
205
+ self.logger.info("Installation cancelled by user")
206
+ return False, []
207
+ else:
208
+ self.logger.warning("Auto-approval enabled, proceeding with installation without user consent")
209
+
210
+ # Step 7: Execute installation plan using installer registry
211
+ installed_packages = self._execute_install_plan(install_plan, env_path, env_name)
212
+
213
+ return True, installed_packages
214
+
215
+ except Exception as e:
216
+ self.logger.error(f"Dependency installation failed: {e}")
217
+ raise DependencyInstallationError(f"Installation failed: {e}") from e
218
+
219
+ def _resolve_and_load_package(self,
220
+ package_path_or_name: str,
221
+ version_constraint: Optional[str] = None,
222
+ force_download: bool = False) -> None:
223
+ """Resolve package information and load metadata using PackageService.
224
+
225
+ Args:
226
+ package_path_or_name (str): Path to local package or name of remote package.
227
+ version_constraint (str, optional): Version constraint for remote packages.
228
+ force_download (bool, optional): Force download even if package is cached.
229
+
230
+ Raises:
231
+ DependencyInstallationError: If package cannot be resolved or loaded.
232
+ """
233
+ path = Path(package_path_or_name)
234
+
235
+ if path.exists() and path.is_dir():
236
+ # Local package
237
+ metadata_path = path / "hatch_metadata.json"
238
+ if not metadata_path.exists():
239
+ raise DependencyInstallationError(f"Local package missing hatch_metadata.json: {path}")
240
+
241
+ with open(metadata_path, 'r') as f:
242
+ metadata = json.load(f)
243
+
244
+ self._resolved_package_path = path
245
+ self._resolved_package_type = "local"
246
+ self._resolved_package_location = str(path.resolve())
247
+
248
+ else:
249
+ # Remote package
250
+ if not self.registry_service.package_exists(package_path_or_name):
251
+ raise DependencyInstallationError(f"Package {package_path_or_name} does not exist in registry")
252
+
253
+ try:
254
+ compatible_version = self.registry_service.find_compatible_version(
255
+ package_path_or_name, version_constraint)
256
+ except VersionConstraintError as e:
257
+ raise DependencyInstallationError(f"Version constraint error: {e}") from e
258
+
259
+ location = self.registry_service.get_package_uri(package_path_or_name, compatible_version)
260
+ downloaded_path = self.package_loader.download_package(
261
+ location, package_path_or_name, compatible_version, force_download=force_download)
262
+
263
+ metadata_path = downloaded_path / "hatch_metadata.json"
264
+ with open(metadata_path, 'r') as f:
265
+ metadata = json.load(f)
266
+
267
+ self._resolved_package_path = downloaded_path
268
+ self._resolved_package_type = "remote"
269
+ self._resolved_package_location = location
270
+
271
+ # Load metadata using PackageService for schema-aware access
272
+ self.package_service = PackageService(metadata)
273
+ if not self.package_service.is_loaded():
274
+ raise DependencyInstallationError("Failed to load package metadata")
275
+
276
+ def _get_install_ready_hatch_dependencies(self) -> List[Dict[str, Any]]:
277
+ """Get install-ready Hatch dependencies using validator components.
278
+
279
+ This method only processes Hatch package dependencies, not python, system, or docker.
280
+
281
+ Returns:
282
+ List[Dict[str, Any]]: List of install-ready Hatch dependencies.
283
+
284
+ Raises:
285
+ DependencyInstallationError: If dependency resolution fails.
286
+ """
287
+ try:
288
+ # Use validator components for Hatch dependency resolution
289
+ self.dependency_graph_builder = HatchDependencyGraphBuilder(
290
+ self.package_service, self.registry_service)
291
+
292
+ context = ValidationContext(
293
+ package_dir=self._resolved_package_path,
294
+ registry_data=self.registry_data,
295
+ allow_local_dependencies=True
296
+ )
297
+
298
+ # This only returns Hatch dependencies in install order
299
+ hatch_dependencies = self.dependency_graph_builder.get_install_ready_dependencies(context)
300
+ return hatch_dependencies
301
+
302
+ except Exception as e:
303
+ raise DependencyInstallationError(f"Error building Hatch dependency graph: {e}") from e
304
+
305
+ def _get_all_dependencies(self) -> Dict[str, List[Dict[str, Any]]]:
306
+ """Get all dependencies from package metadata organized by type.
307
+
308
+ Returns:
309
+ Dict[str, List[Dict[str, Any]]]: Dependencies organized by type (hatch, python, system, docker).
310
+
311
+ Raises:
312
+ DependencyInstallationError: If dependency extraction fails.
313
+ """
314
+ try:
315
+ # Get all dependencies using PackageService
316
+ all_deps = self.package_service.get_dependencies()
317
+
318
+ dependencies_by_type = {
319
+ "system": [],
320
+ "python": [],
321
+ "hatch": [],
322
+ "docker": []
323
+ }
324
+
325
+ # Get Hatch dependencies using validator (properly ordered)
326
+ dependencies_by_type["hatch"] = self._get_install_ready_hatch_dependencies()
327
+ # Adding the type information to each Hatch dependency
328
+ for dep in dependencies_by_type["hatch"]:
329
+ dep["type"] = "hatch"
330
+
331
+ # Get other dependency types directly from PackageService
332
+ for dep_type in ["python", "system", "docker"]:
333
+ raw_deps = all_deps.get(dep_type, [])
334
+ for dep in raw_deps:
335
+
336
+ # Add type information and ensure required fields
337
+ dep_with_type = dep.copy()
338
+ dep_with_type["type"] = dep_type
339
+ if not installer_registry.can_install(dep_type, dep_with_type):
340
+ raise DependencyInstallationError(
341
+ f"No registered installer can handle dependency with type '{dep_type}': {dep_with_type}"
342
+ )
343
+
344
+ dependencies_by_type[dep_type].append(dep_with_type)
345
+
346
+ return dependencies_by_type
347
+
348
+ except Exception as e:
349
+ raise DependencyInstallationError(f"Error extracting dependencies: {e}") from e
350
+
351
+ def _filter_missing_dependencies_by_type(self,
352
+ dependencies_by_type: Dict[str, List[Dict[str, Any]]],
353
+ existing_packages: Dict[str, str]) -> Tuple[Dict[str, List[Dict[str, Any]]], Dict[str, List[Dict[str, Any]]]]:
354
+ """Filter dependencies by type to find those not already installed and track satisfied ones.
355
+
356
+ For non-Hatch dependencies, we always include them in missing list as the third-party
357
+ package manager will handle version checking and installation.
358
+
359
+ Args:
360
+ dependencies_by_type (Dict[str, List[Dict[str, Any]]]): All dependencies organized by type.
361
+ existing_packages (Dict[str, str]): Currently installed packages {name: version}.
362
+
363
+ Returns:
364
+ Tuple[Dict[str, List[Dict[str, Any]]], Dict[str, List[Dict[str, Any]]]]:
365
+ (missing_dependencies_by_type, satisfied_dependencies_by_type)
366
+ """
367
+ missing_deps_by_type = {}
368
+ satisfied_deps_by_type = {}
369
+
370
+ for dep_type, dependencies in dependencies_by_type.items():
371
+ missing_deps = []
372
+ satisfied_deps = []
373
+
374
+ for dep in dependencies:
375
+ dep_name = dep.get("name")
376
+
377
+ # For non-Hatch dependencies, always consider them as needing installation
378
+ # as the third-party package manager will handle version compatibility
379
+ if dep_type != "hatch":
380
+ missing_deps.append(dep)
381
+ continue
382
+
383
+ # Hatch dependency processing
384
+ if dep_name not in existing_packages:
385
+ missing_deps.append(dep)
386
+ continue
387
+
388
+ # Check version constraints for Hatch dependencies
389
+ constraint = dep.get("version_constraint")
390
+ installed_version = existing_packages[dep_name]
391
+
392
+ if constraint:
393
+ is_compatible, compatibility_msg = VersionConstraintValidator.is_version_compatible(
394
+ installed_version, constraint)
395
+ if not is_compatible:
396
+ missing_deps.append(dep)
397
+ else:
398
+ # Add satisfied dependency with installation info
399
+ satisfied_dep = dep.copy()
400
+ satisfied_dep["installed_version"] = installed_version
401
+ satisfied_dep["compatibility_status"] = compatibility_msg
402
+ satisfied_deps.append(satisfied_dep)
403
+ else:
404
+ # No constraint specified, any installed version satisfies
405
+ satisfied_dep = dep.copy()
406
+ satisfied_dep["installed_version"] = installed_version
407
+ satisfied_dep["compatibility_status"] = "No version constraint specified"
408
+ satisfied_deps.append(satisfied_dep)
409
+
410
+ missing_deps_by_type[dep_type] = missing_deps
411
+ satisfied_deps_by_type[dep_type] = satisfied_deps
412
+
413
+ return missing_deps_by_type, satisfied_deps_by_type
414
+
415
+ def _aggregate_install_plan(self,
416
+ missing_dependencies_by_type: Dict[str, List[Dict[str, Any]]],
417
+ satisfied_dependencies_by_type: Dict[str, List[Dict[str, Any]]]) -> Dict[str, Any]:
418
+ """Aggregate installation plan across all dependency types.
419
+
420
+ Args:
421
+ missing_dependencies_by_type (Dict[str, List[Dict[str, Any]]]): Missing dependencies by type.
422
+ satisfied_dependencies_by_type (Dict[str, List[Dict[str, Any]]]): Already satisfied dependencies by type.
423
+
424
+ Returns:
425
+ Dict[str, Any]: Complete installation plan with dependencies grouped by type.
426
+ """
427
+ # Use PackageService for all metadata access
428
+ plan = {
429
+ "main_package": {
430
+ "name": self.package_service.get_field("name"),
431
+ "version": self.package_service.get_field("version"),
432
+ "type": self._resolved_package_type,
433
+ "location": self._resolved_package_location
434
+ },
435
+ "dependencies_to_install": missing_dependencies_by_type,
436
+ "dependencies_satisfied": satisfied_dependencies_by_type,
437
+ "total_to_install": 1 + sum(len(deps) for deps in missing_dependencies_by_type.values()),
438
+ "total_satisfied": sum(len(deps) for deps in satisfied_dependencies_by_type.values())
439
+ }
440
+
441
+ return plan
442
+
443
+ def _print_installation_summary(self, install_plan: Dict[str, Any]) -> None:
444
+ """Print a summary of the installation plan for user review.
445
+
446
+ Args:
447
+ install_plan (Dict[str, Any]): Complete installation plan.
448
+ """
449
+ print("\n" + "="*60)
450
+ print("DEPENDENCY INSTALLATION PLAN")
451
+ print("="*60)
452
+
453
+ main_pkg = install_plan['main_package']
454
+ print(f"Main Package: {main_pkg['name']} v{main_pkg['version']}")
455
+ print(f"Package Type: {main_pkg['type']}")
456
+
457
+ # Show satisfied dependencies first
458
+ total_satisfied = install_plan.get("total_satisfied", 0)
459
+ if total_satisfied > 0:
460
+ print(f"\nDependencies already satisfied: {total_satisfied}")
461
+
462
+ for dep_type, deps in install_plan.get("dependencies_satisfied", {}).items():
463
+ if deps:
464
+ print(f"\n{dep_type.title()} Dependencies (Satisfied):")
465
+ for dep in deps:
466
+ installed_version = dep.get("installed_version", "unknown")
467
+ constraint = dep.get("version_constraint", "any")
468
+ compatibility = dep.get("compatibility_status", "")
469
+ print(f" ✓ {dep['name']} {constraint} (installed: {installed_version})")
470
+ if compatibility and compatibility != "No version constraint specified":
471
+ print(f" {compatibility}")
472
+
473
+ # Show dependencies to install
474
+ total_to_install = sum(len(deps) for deps in install_plan.get("dependencies_to_install", {}).values())
475
+ if total_to_install > 0:
476
+ print(f"\nDependencies to install: {total_to_install}")
477
+
478
+ for dep_type, deps in install_plan.get("dependencies_to_install", {}).items():
479
+ if deps:
480
+ print(f"\n{dep_type.title()} Dependencies (To Install):")
481
+ for dep in deps:
482
+ constraint = dep.get("version_constraint", "any")
483
+ print(f" → {dep['name']} {constraint}")
484
+ else:
485
+ print("\nNo additional dependencies to install.")
486
+
487
+ print(f"\nTotal packages to install: {install_plan.get('total_to_install', 1)}")
488
+ if total_satisfied > 0:
489
+ print(f"Total dependencies already satisfied: {total_satisfied}")
490
+ print("="*60)
491
+
492
+ def _request_user_consent(self, install_plan: Dict[str, Any]) -> bool:
493
+ """Request user consent for the installation plan with non-TTY support.
494
+
495
+ Args:
496
+ install_plan (Dict[str, Any]): Complete installation plan.
497
+
498
+ Returns:
499
+ bool: True if user approves, False otherwise.
500
+ """
501
+ # Check for non-interactive mode indicators
502
+ if (not sys.stdin.isatty() or
503
+ os.getenv('HATCH_AUTO_APPROVE', '').lower() in ('1', 'true', 'yes')):
504
+
505
+ self.logger.info("Auto-approving installation (non-interactive mode)")
506
+ return True
507
+
508
+ # Interactive mode - request user input
509
+ try:
510
+ while True:
511
+ response = input("\nProceed with installation? [y/N]: ").strip().lower()
512
+ if response in ['y', 'yes']:
513
+ return True
514
+ elif response in ['n', 'no', '']:
515
+ return False
516
+ else:
517
+ print("Please enter 'y' for yes or 'n' for no.")
518
+ except (EOFError, KeyboardInterrupt):
519
+ self.logger.info("Installation cancelled by user")
520
+ return False
521
+
522
+ def _execute_install_plan(self,
523
+ install_plan: Dict[str, Any],
524
+ env_path: Path,
525
+ env_name: str) -> List[Dict[str, Any]]:
526
+ """Execute the installation plan using the installer registry.
527
+
528
+ Args:
529
+ install_plan (Dict[str, Any]): Installation plan to execute.
530
+ env_path (Path): Environment path for installation.
531
+ env_name (str): Environment name.
532
+
533
+ Returns:
534
+ List[Dict[str, Any]]: List of successfully installed packages.
535
+
536
+ Raises:
537
+ DependencyInstallationError: If installation fails.
538
+ """
539
+ installed_packages = []
540
+
541
+ # Create comprehensive installation context
542
+ context = InstallationContext(
543
+ environment_path=env_path,
544
+ environment_name=env_name,
545
+ temp_dir=env_path / ".tmp",
546
+ cache_dir=self.package_loader.cache_dir if hasattr(self.package_loader, 'cache_dir') else None,
547
+ parallel_enabled=False, # Future enhancement
548
+ force_reinstall=False, # Future enhancement
549
+ simulation_mode=False, # Future enhancement
550
+ extra_config={
551
+ "package_loader": self.package_loader,
552
+ "registry_service": self.registry_service,
553
+ "registry_data": self.registry_data,
554
+ "main_package_path": self._resolved_package_path,
555
+ "main_package_type": self._resolved_package_type
556
+ }
557
+ )
558
+
559
+ # Configure Python environment variables if available
560
+ if self._python_env_vars:
561
+ context.set_config("python_env_vars", self._python_env_vars)
562
+
563
+ try:
564
+ # Install dependencies by type using appropriate installers
565
+ for dep_type, dependencies in install_plan["dependencies_to_install"].items():
566
+ if not dependencies:
567
+ continue
568
+
569
+ if not installer_registry.is_registered(dep_type):
570
+ self.logger.warning(f"No installer registered for dependency type: {dep_type}")
571
+ continue
572
+
573
+ installer = installer_registry.get_installer(dep_type)
574
+
575
+ for dep in dependencies:
576
+ # Use the extracted install_single_dep method
577
+ installed_package = self.install_single_dep(dep, context)
578
+ installed_packages.append(installed_package)
579
+
580
+ # Install main package last
581
+ main_pkg_info = self._install_main_package(context)
582
+ installed_packages.append(main_pkg_info)
583
+
584
+ return installed_packages
585
+
586
+ except Exception as e:
587
+ self.logger.error(f"Installation execution failed: {e}")
588
+ raise DependencyInstallationError(f"Installation execution failed: {e}") from e
589
+
590
+ def _install_main_package(self, context: InstallationContext) -> Dict[str, Any]:
591
+ """Install the main package using package_loader directly.
592
+
593
+ The main package installation bypasses the installer registry and uses
594
+ the package_loader directly since it's not a dependency but the primary package.
595
+
596
+ Args:
597
+ context (InstallationContext): Installation context.
598
+
599
+ Returns:
600
+ Dict[str, Any]: Installed package information.
601
+
602
+ Raises:
603
+ DependencyInstallationError: If main package installation fails.
604
+ """
605
+ try:
606
+ # Get package information using PackageService
607
+ package_name = self.package_service.get_field("name")
608
+ package_version = self.package_service.get_field("version")
609
+
610
+ # Install using package_loader directly
611
+ if self._resolved_package_type == "local":
612
+ # For local packages, install from resolved path
613
+ installed_path = self.package_loader.install_local_package(
614
+ source_path=self._resolved_package_path,
615
+ target_dir=context.environment_path,
616
+ package_name=package_name
617
+ )
618
+ else:
619
+ # For remote packages, install from downloaded path
620
+ installed_path = self.package_loader.install_local_package(
621
+ source_path=self._resolved_package_path, # Downloaded path
622
+ target_dir=context.environment_path,
623
+ package_name=package_name
624
+ )
625
+
626
+ self.logger.info(f"Successfully installed main package {package_name} to {installed_path}")
627
+
628
+ return {
629
+ "name": package_name,
630
+ "version": package_version,
631
+ "type": "hatch",
632
+ "source": self._resolved_package_location
633
+ }
634
+
635
+ except Exception as e:
636
+ raise DependencyInstallationError(f"Failed to install main package: {e}") from e