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,342 @@
1
+ """Installer for Python package dependencies using pip.
2
+
3
+ This module implements installation logic for Python packages using pip via subprocess,
4
+ with support for configurable Python environments and comprehensive error handling.
5
+ """
6
+
7
+ import sys
8
+ import subprocess
9
+ import logging
10
+ import os
11
+ import json
12
+ from pathlib import Path
13
+ from typing import Dict, Any, Optional, Callable, List
14
+ import os
15
+ from pathlib import Path
16
+ from typing import Dict, Any, Optional, Callable, List
17
+
18
+ from .installer_base import DependencyInstaller, InstallationContext, InstallationResult, InstallationError
19
+ from .installation_context import InstallationStatus
20
+
21
+
22
+ class PythonInstaller(DependencyInstaller):
23
+ """Installer for Python package dependencies using pip.
24
+
25
+ Handles installation of Python packages using pip via subprocess, with support
26
+ for configurable Python environments through InstallationContext.extra_config.
27
+ """
28
+
29
+ def __init__(self):
30
+ """Initialize the PythonInstaller."""
31
+ self.logger = logging.getLogger("hatch.installers.python_installer")
32
+ self.logger.setLevel(logging.INFO)
33
+
34
+ @property
35
+ def installer_type(self) -> str:
36
+ """Get the type identifier for this installer.
37
+
38
+ Returns:
39
+ str: Unique identifier for the installer type ("python").
40
+ """
41
+ return "python"
42
+
43
+ @property
44
+ def supported_schemes(self) -> List[str]:
45
+ """Get the URI schemes this installer can handle.
46
+
47
+ This installer supports:
48
+ - "pypi" for PyPI packages
49
+ - "git+https" for Git repositories over HTTPS
50
+ - "git+ssh" for Git repositories over SSH
51
+ - "file" for local file paths
52
+
53
+ Returns:
54
+ List[str]: List of URI schemes (["pypi", "git+https", "git+ssh", "file"]).
55
+ """
56
+ return ["pypi", "git+https", "git+ssh", "file"]
57
+
58
+ def can_install(self, dependency: Dict[str, Any]) -> bool:
59
+ """Check if this installer can handle the given dependency.
60
+
61
+ Args:
62
+ dependency (Dict[str, Any]): Dependency object.
63
+
64
+ Returns:
65
+ bool: True if this installer can handle the dependency, False otherwise.
66
+ """
67
+ return dependency.get("type") == self.installer_type
68
+
69
+ def validate_dependency(self, dependency: Dict[str, Any]) -> bool:
70
+ """Validate that a dependency object has required fields for Python packages.
71
+
72
+ Args:
73
+ dependency (Dict[str, Any]): Dependency object to validate.
74
+
75
+ Returns:
76
+ bool: True if dependency is valid, False otherwise.
77
+ """
78
+ required_fields = ["name", "version_constraint"]
79
+ if not all(field in dependency for field in required_fields):
80
+ return False
81
+
82
+ # Check for valid package manager if specified
83
+ package_manager = dependency.get("package_manager", "pip")
84
+ if package_manager not in ["pip"]:
85
+ return False
86
+
87
+ return True
88
+
89
+ def _run_pip_subprocess(self, cmd: List[str], env_vars: Dict[str, str] = None) -> int:
90
+ """Run a pip subprocess and return the exit code.
91
+
92
+ Args:
93
+ cmd (List[str]): The pip command to execute as a list.
94
+ env_vars (Dict[str, str], optional): Additional environment variables to set for the subprocess.
95
+
96
+ Returns:
97
+ int: The return code of the pip subprocess.
98
+
99
+ Raises:
100
+ subprocess.TimeoutExpired: If the process times out.
101
+ Exception: For unexpected errors.
102
+ """
103
+
104
+ env = os.environ.copy()
105
+ env['PYTHONUNBUFFERED'] = '1'
106
+ env.update(env_vars or {}) # Merge in any additional environment variables
107
+
108
+ self.logger.debug(f"Running pip command: {' '.join(cmd)} with env: {json.dumps(env, indent=2)}")
109
+
110
+ try:
111
+ result = subprocess.run(
112
+ cmd,
113
+ env=env,
114
+ check=False, # Don't raise on non-zero exit codes
115
+ timeout=300 # 5 minute timeout
116
+ )
117
+
118
+ return result.returncode
119
+
120
+ except subprocess.TimeoutExpired:
121
+ raise InstallationError("Pip subprocess timed out", error_code="TIMEOUT", cause=None)
122
+
123
+ except Exception as e:
124
+ raise InstallationError(
125
+ f"Unexpected error running pip command: {e}",
126
+ error_code="PIP_SUBPROCESS_ERROR",
127
+ cause=e
128
+ )
129
+
130
+ def install(self, dependency: Dict[str, Any], context: InstallationContext,
131
+ progress_callback: Optional[Callable[[str, float, str], None]] = None) -> InstallationResult:
132
+ """Install a Python package dependency using pip.
133
+
134
+ This method uses subprocess to call pip with the appropriate Python executable,
135
+ which can be configured via context.extra_config["python_executable"].
136
+
137
+ Args:
138
+ dependency (Dict[str, Any]): Dependency object containing name, version, etc.
139
+ context (InstallationContext): Installation context with environment info.
140
+ progress_callback (Callable[[str, float, str], None], optional): Progress reporting callback.
141
+
142
+ Returns:
143
+ InstallationResult: Result of the installation operation.
144
+
145
+ Raises:
146
+ InstallationError: If installation fails for any reason.
147
+ """
148
+ name = dependency["name"]
149
+ version_constraint = dependency["version_constraint"]
150
+
151
+ if progress_callback:
152
+ progress_callback("validate", 0.0, f"Validating Python package {name}")
153
+
154
+ # Get Python executable from context or use system default
155
+ python_env_vars = context.get_config("python_env_vars", {})
156
+ self.logger.debug(f"Using Python environment variables: {python_env_vars}")
157
+ python_exec = python_env_vars.get("PYTHON", sys.executable)
158
+ self.logger.debug(f"Using Python executable: {python_exec}")
159
+
160
+ # Build package specification with version constraint
161
+ # Let pip resolve the actual version based on the constraint
162
+ if version_constraint and version_constraint != "*":
163
+ package_spec = f"{name}{version_constraint}"
164
+ else:
165
+ package_spec = name
166
+
167
+ # Handle extras if specified
168
+ extras = dependency.get("extras", [])
169
+ if extras:
170
+ if isinstance(extras, list):
171
+ extras_str = ",".join(extras)
172
+ else:
173
+ extras_str = str(extras)
174
+ if version_constraint and version_constraint != "*":
175
+ package_spec = f"{name}[{extras_str}]{version_constraint}"
176
+ else:
177
+ package_spec = f"{name}[{extras_str}]"
178
+
179
+ # Build pip command
180
+ self.logger.debug(f"Installing Python package: {package_spec} using {python_exec}")
181
+ cmd = [str(python_exec), "-m", "pip", "install", package_spec]
182
+
183
+ # Add additional pip options
184
+ cmd.extend(["--no-cache-dir"]) # Avoid cache issues in different environments
185
+
186
+ if context.simulation_mode:
187
+ # In simulation mode, just return success without actually installing
188
+ self.logger.info(f"Simulation mode: would install {package_spec}")
189
+ return InstallationResult(
190
+ dependency_name=name,
191
+ status=InstallationStatus.COMPLETED,
192
+ installed_version=version_constraint,
193
+ metadata={"simulation": True, "command": cmd}
194
+ )
195
+
196
+ try:
197
+ if progress_callback:
198
+ progress_callback("install", 0.3, f"Installing {package_spec}")
199
+
200
+ returncode = self._run_pip_subprocess(cmd, env_vars=python_env_vars)
201
+ self.logger.debug(f"pip command: {' '.join(cmd)}\nreturncode: {returncode}")
202
+
203
+ if returncode == 0:
204
+
205
+ if progress_callback:
206
+ progress_callback("install", 1.0, f"Successfully installed {name}")
207
+
208
+ return InstallationResult(
209
+ dependency_name=name,
210
+ status=InstallationStatus.COMPLETED,
211
+ metadata={
212
+ "command": cmd,
213
+ "version_constraint": version_constraint
214
+ }
215
+ )
216
+
217
+ else:
218
+ error_msg = f"Failed to install {name} (exit code: {returncode})"
219
+ self.logger.error(error_msg)
220
+ raise InstallationError(
221
+ error_msg,
222
+ dependency_name=name,
223
+ error_code="PIP_FAILED",
224
+ cause=None
225
+ )
226
+ except subprocess.TimeoutExpired:
227
+ error_msg = f"Installation of {name} timed out after 5 minutes"
228
+ self.logger.error(error_msg)
229
+ raise InstallationError(error_msg, dependency_name=name, error_code="TIMEOUT")
230
+
231
+ except Exception as e:
232
+ error_msg = f"Unexpected error installing {name}: {repr(e)}"
233
+ self.logger.error(error_msg)
234
+ raise InstallationError(error_msg, dependency_name=name, cause=e)
235
+
236
+ def uninstall(self, dependency: Dict[str, Any], context: InstallationContext,
237
+ progress_callback: Optional[Callable[[str, float, str], None]] = None) -> InstallationResult:
238
+ """Uninstall a Python package dependency using pip.
239
+
240
+ Args:
241
+ dependency (Dict[str, Any]): Dependency object to uninstall.
242
+ context (InstallationContext): Installation context with environment info.
243
+ progress_callback (Callable[[str, float, str], None], optional): Progress reporting callback.
244
+
245
+ Returns:
246
+ InstallationResult: Result of the uninstall operation.
247
+
248
+ Raises:
249
+ InstallationError: If uninstall fails for any reason.
250
+ """
251
+ name = dependency["name"]
252
+
253
+ if progress_callback:
254
+ progress_callback("uninstall", 0.0, f"Uninstalling Python package {name}")
255
+
256
+ # Get Python executable from context
257
+ python_env_vars = context.get_config("python_env_vars", {})
258
+ # Use the configured Python executable or fall back to system default
259
+ python_exec = python_env_vars.get("PYTHON", sys.executable)
260
+
261
+ # Build pip uninstall command
262
+ cmd = [str(python_exec), "-m", "pip", "uninstall", "-y", name]
263
+
264
+ if context.simulation_mode:
265
+ self.logger.info(f"Simulation mode: would uninstall {name}")
266
+ return InstallationResult(
267
+ dependency_name=name,
268
+ status=InstallationStatus.COMPLETED,
269
+ metadata={"simulation": True, "command": cmd}
270
+ )
271
+
272
+ try:
273
+ if progress_callback:
274
+ progress_callback("uninstall", 0.5, f"Removing {name}")
275
+
276
+ returncode = self._run_pip_subprocess(cmd, env_vars=python_env_vars)
277
+
278
+ if returncode == 0:
279
+
280
+ if progress_callback:
281
+ progress_callback("uninstall", 1.0, f"Successfully uninstalled {name}")
282
+ self.logger.info(f"Successfully uninstalled Python package {name}")
283
+
284
+ return InstallationResult(
285
+ dependency_name=name,
286
+ status=InstallationStatus.COMPLETED,
287
+ metadata={
288
+ "command": cmd
289
+ }
290
+ )
291
+ else:
292
+ error_msg = f"Failed to uninstall {name} (exit code: {returncode})"
293
+ self.logger.error(error_msg)
294
+
295
+ raise InstallationError(
296
+ error_msg,
297
+ dependency_name=name,
298
+ error_code="PIP_UNINSTALL_FAILED",
299
+ cause=None
300
+ )
301
+ except subprocess.TimeoutExpired:
302
+ error_msg = f"Uninstallation of {name} timed out after 1 minute"
303
+ self.logger.error(error_msg)
304
+ raise InstallationError(error_msg, dependency_name=name, error_code="TIMEOUT")
305
+ except Exception as e:
306
+ error_msg = f"Unexpected error uninstalling {name}: {e}"
307
+ self.logger.error(error_msg)
308
+ raise InstallationError(error_msg, dependency_name=name, cause=e)
309
+
310
+ def get_installation_info(self, dependency: Dict[str, Any], context: InstallationContext) -> Dict[str, Any]:
311
+ """Get information about what would be installed without actually installing.
312
+
313
+ Args:
314
+ dependency (Dict[str, Any]): Dependency object to analyze.
315
+ context (InstallationContext): Installation context.
316
+
317
+ Returns:
318
+ Dict[str, Any]: Information about the planned installation.
319
+ """
320
+ python_exec = context.get_config("python_executable", sys.executable)
321
+ version_constraint = dependency.get("version_constraint", "*")
322
+
323
+ # Build package spec for display
324
+ if version_constraint and version_constraint != "*":
325
+ package_spec = f"{dependency['name']}{version_constraint}"
326
+ else:
327
+ package_spec = dependency['name']
328
+
329
+ info = super().get_installation_info(dependency, context)
330
+ info.update({
331
+ "python_executable": str(python_exec),
332
+ "package_manager": dependency.get("package_manager", "pip"),
333
+ "package_spec": package_spec,
334
+ "version_constraint": version_constraint,
335
+ "extras": dependency.get("extras", []),
336
+ })
337
+
338
+ return info
339
+
340
+ # Register this installer with the global registry
341
+ from .registry import installer_registry
342
+ installer_registry.register_installer("python", PythonInstaller)
@@ -0,0 +1,179 @@
1
+ """Installer registry for dependency installers.
2
+
3
+ This module provides a centralized registry for mapping dependency types to their
4
+ corresponding installer implementations, enabling dynamic lookup and delegation
5
+ of installation operations.
6
+ """
7
+
8
+ import logging
9
+ from typing import Dict, Type, List, Optional, Any
10
+
11
+ from .installer_base import DependencyInstaller
12
+
13
+ logger = logging.getLogger("hatch.installer_registry")
14
+
15
+
16
+ class InstallerRegistry:
17
+ """Registry for dependency installers by type.
18
+
19
+ This class provides a centralized mapping between dependency types and their
20
+ corresponding installer implementations. It enables the orchestrator to remain
21
+ agnostic to installer details while providing extensible installer management.
22
+
23
+ The registry follows these principles:
24
+ - Single source of truth for installer-to-type mappings
25
+ - Dynamic registration and lookup
26
+ - Clear error handling for unsupported types
27
+ - Extensibility for future installer types
28
+ """
29
+
30
+ def __init__(self):
31
+ """Initialize the installer registry."""
32
+ self._installers: Dict[str, Type[DependencyInstaller]] = {}
33
+ logger.debug("Initialized installer registry")
34
+
35
+ def register_installer(self, dep_type: str, installer_cls: Type[DependencyInstaller]) -> None:
36
+ """Register an installer class for a dependency type.
37
+
38
+ Args:
39
+ dep_type (str): The dependency type identifier (e.g., "hatch", "python", "docker").
40
+ installer_cls (Type[DependencyInstaller]): The installer class to register.
41
+
42
+ Raises:
43
+ ValueError: If the installer class does not implement DependencyInstaller.
44
+ TypeError: If the installer_cls is not a class or is None.
45
+ """
46
+ if not isinstance(installer_cls, type):
47
+ raise TypeError(f"installer_cls must be a class, got {type(installer_cls)}")
48
+
49
+ if not issubclass(installer_cls, DependencyInstaller):
50
+ raise ValueError(f"installer_cls must be a subclass of DependencyInstaller, got {installer_cls}")
51
+
52
+ if dep_type in self._installers:
53
+ logger.warning(f"Overriding existing installer for type '{dep_type}': {self._installers[dep_type]} -> {installer_cls}")
54
+
55
+ self._installers[dep_type] = installer_cls
56
+ logger.debug(f"Registered installer for type '{dep_type}': {installer_cls.__name__}")
57
+
58
+ def get_installer(self, dep_type: str) -> DependencyInstaller:
59
+ """Get an installer instance for the given dependency type.
60
+
61
+ Args:
62
+ dep_type (str): The dependency type to get an installer for.
63
+
64
+ Returns:
65
+ DependencyInstaller: A new instance of the appropriate installer.
66
+
67
+ Raises:
68
+ ValueError: If no installer is registered for the given dependency type.
69
+ """
70
+ if dep_type not in self._installers:
71
+ available_types = list(self._installers.keys())
72
+ raise ValueError(
73
+ f"No installer registered for dependency type '{dep_type}'. "
74
+ f"Available types: {available_types}"
75
+ )
76
+
77
+ installer_cls = self._installers[dep_type]
78
+ installer = installer_cls()
79
+ logger.debug(f"Created installer instance for type '{dep_type}': {installer_cls.__name__}")
80
+ return installer
81
+
82
+ def can_install(self, dep_type: str, dependency: Dict[str, Any]) -> bool:
83
+ """Check if the registry can handle the given dependency.
84
+
85
+ This method first checks if an installer is registered for the dependency's
86
+ type, then delegates to the installer's can_install method for more
87
+ detailed validation.
88
+
89
+ Args:
90
+ dependency (Dict[str, Any]): Dependency object to check.
91
+
92
+ Returns:
93
+ bool: True if the dependency can be installed, False otherwise.
94
+ """
95
+ if dep_type not in self._installers:
96
+ logger.error(f"No installer registered for dependency type '{dep_type}'")
97
+ return False
98
+
99
+ try:
100
+ installer = self.get_installer(dep_type)
101
+ return installer.can_install(dependency)
102
+ except Exception as e:
103
+ logger.warning(f"Error checking if dependency can be installed: {e}")
104
+ return False
105
+
106
+ def get_registered_types(self) -> List[str]:
107
+ """Get a list of all registered dependency types.
108
+
109
+ Returns:
110
+ List[str]: List of registered dependency type identifiers.
111
+ """
112
+ return list(self._installers.keys())
113
+
114
+ def is_registered(self, dep_type: str) -> bool:
115
+ """Check if an installer is registered for the given type.
116
+
117
+ Args:
118
+ dep_type (str): The dependency type to check.
119
+
120
+ Returns:
121
+ bool: True if an installer is registered for the type, False otherwise.
122
+ """
123
+ return dep_type in self._installers
124
+
125
+ def unregister_installer(self, dep_type: str) -> Optional[Type[DependencyInstaller]]:
126
+ """Unregister an installer for the given dependency type.
127
+
128
+ This method is primarily intended for testing and advanced use cases.
129
+
130
+ Args:
131
+ dep_type (str): The dependency type to unregister.
132
+
133
+ Returns:
134
+ Type[DependencyInstaller]: The unregistered installer class, or None if not found.
135
+ """
136
+ installer_cls = self._installers.pop(dep_type, None)
137
+ if installer_cls:
138
+ logger.debug(f"Unregistered installer for type '{dep_type}': {installer_cls.__name__}")
139
+ return installer_cls
140
+
141
+ def clear(self) -> None:
142
+ """Clear all registered installers.
143
+
144
+ This method is primarily intended for testing purposes.
145
+ """
146
+ self._installers.clear()
147
+ logger.debug("Cleared all registered installers")
148
+
149
+ def __len__(self) -> int:
150
+ """Get the number of registered installers.
151
+
152
+ Returns:
153
+ int: Number of registered installers.
154
+ """
155
+ return len(self._installers)
156
+
157
+ def __contains__(self, dep_type: str) -> bool:
158
+ """Check if a dependency type is registered.
159
+
160
+ Args:
161
+ dep_type (str): The dependency type to check.
162
+
163
+ Returns:
164
+ bool: True if the type is registered, False otherwise.
165
+ """
166
+ return dep_type in self._installers
167
+
168
+ def __repr__(self) -> str:
169
+ """Get a string representation of the registry.
170
+
171
+ Returns:
172
+ str: String representation showing registered types.
173
+ """
174
+ types = list(self._installers.keys())
175
+ return f"InstallerRegistry(types={types})"
176
+
177
+
178
+ # Global singleton instance
179
+ installer_registry = InstallerRegistry()