more-compute 0.1.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.
- kernel_run.py +283 -0
- more_compute-0.1.0.dist-info/METADATA +163 -0
- more_compute-0.1.0.dist-info/RECORD +26 -0
- more_compute-0.1.0.dist-info/WHEEL +5 -0
- more_compute-0.1.0.dist-info/entry_points.txt +2 -0
- more_compute-0.1.0.dist-info/licenses/LICENSE +21 -0
- more_compute-0.1.0.dist-info/top_level.txt +2 -0
- morecompute/__init__.py +6 -0
- morecompute/cli.py +31 -0
- morecompute/execution/__init__.py +5 -0
- morecompute/execution/__main__.py +10 -0
- morecompute/execution/executor.py +381 -0
- morecompute/execution/worker.py +244 -0
- morecompute/notebook.py +81 -0
- morecompute/process_worker.py +209 -0
- morecompute/server.py +641 -0
- morecompute/services/pod_manager.py +503 -0
- morecompute/services/prime_intellect.py +316 -0
- morecompute/static/styles.css +1056 -0
- morecompute/utils/__init__.py +17 -0
- morecompute/utils/cache_util.py +23 -0
- morecompute/utils/error_utils.py +322 -0
- morecompute/utils/notebook_util.py +44 -0
- morecompute/utils/python_environment_util.py +197 -0
- morecompute/utils/special_commands.py +458 -0
- morecompute/utils/system_environment_util.py +134 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility modules for MoreCompute notebook
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .python_environment_util import PythonEnvironmentDetector
|
|
6
|
+
from .system_environment_util import DeviceMetrics
|
|
7
|
+
from .error_utils import ErrorUtils
|
|
8
|
+
from .cache_util import make_cache_key
|
|
9
|
+
from .notebook_util import coerce_cell_source
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
'PythonEnvironmentDetector',
|
|
13
|
+
'DeviceMetrics',
|
|
14
|
+
'ErrorUtils',
|
|
15
|
+
'make_cache_key',
|
|
16
|
+
'coerce_cell_source'
|
|
17
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def make_cache_key(prefix: str, **params) -> str:
|
|
6
|
+
"""
|
|
7
|
+
Create a cache key from parameters.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
prefix: Cache key prefix (e.g., "gpu_avail", "pod_list")
|
|
11
|
+
**params: Parameters to hash into the key
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
Cache key string in format "prefix:hash"
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
>>> make_cache_key("gpu_avail", regions=["us"], gpu_type="H100")
|
|
18
|
+
"gpu_avail:a3f2c1e8"
|
|
19
|
+
"""
|
|
20
|
+
# Sort params for consistency: {a:1, b:2} == {b:2, a:1}
|
|
21
|
+
param_str = json.dumps(params, sort_keys=True, default=str)
|
|
22
|
+
hash_val = hashlib.md5(param_str.encode()).hexdigest()[:8]
|
|
23
|
+
return f"{prefix}:{hash_val}"
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import traceback
|
|
3
|
+
from typing import Dict, Any, List
|
|
4
|
+
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
most of this is ai-generated, but I would like custom error handling for most general errors
|
|
8
|
+
|
|
9
|
+
imagine like user does not know pip is ran with !pip install, and just runs pip install,
|
|
10
|
+
|
|
11
|
+
it would be nice just to have a custom error message pointing user to use !pip rather than just fail, etc
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def create_enhanced_error_info(exception: Exception, traceback_lines: List[str]) -> Dict[str, Any]:
|
|
20
|
+
"""
|
|
21
|
+
Create enhanced error information with better formatting and suggestions
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
exception: The caught exception
|
|
25
|
+
traceback_lines: List of traceback lines
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Enhanced error dictionary with suggestions and formatting
|
|
29
|
+
"""
|
|
30
|
+
error_type = type(exception).__name__
|
|
31
|
+
error_message = str(exception)
|
|
32
|
+
|
|
33
|
+
# Check for specific error types and provide custom handling
|
|
34
|
+
if _is_pip_related_error(error_message, traceback_lines):
|
|
35
|
+
return _create_pip_error_info(error_type, error_message, traceback_lines)
|
|
36
|
+
elif _is_import_error(error_type, error_message):
|
|
37
|
+
return _create_import_error_info(error_type, error_message, traceback_lines)
|
|
38
|
+
elif _is_file_not_found_error(error_type, error_message):
|
|
39
|
+
return _create_file_error_info(error_type, error_message, traceback_lines)
|
|
40
|
+
else:
|
|
41
|
+
return _create_generic_enhanced_error(error_type, error_message, traceback_lines)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _is_pip_related_error(error_message: str, traceback_lines: List[str]) -> bool:
|
|
45
|
+
"""Check if the error is related to pip operations"""
|
|
46
|
+
pip_indicators = [
|
|
47
|
+
"pip install",
|
|
48
|
+
"pip uninstall",
|
|
49
|
+
"pip show",
|
|
50
|
+
"pip list",
|
|
51
|
+
"subprocess.*pip",
|
|
52
|
+
"ModuleNotFoundError.*pip",
|
|
53
|
+
"No module named 'pip'"
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
combined_text = error_message + " " + " ".join(traceback_lines)
|
|
57
|
+
|
|
58
|
+
# Check for direct pip command syntax errors (most common case)
|
|
59
|
+
if "SyntaxError" in error_message and "pip install" in combined_text:
|
|
60
|
+
return True
|
|
61
|
+
|
|
62
|
+
return any(re.search(indicator, combined_text, re.IGNORECASE) for indicator in pip_indicators)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _is_import_error(error_type: str, error_message: str) -> bool:
|
|
66
|
+
"""Check if this is a module import error"""
|
|
67
|
+
return error_type in ["ModuleNotFoundError", "ImportError"]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _is_file_not_found_error(error_type: str, error_message: str) -> bool:
|
|
71
|
+
"""Check if this is a file not found error"""
|
|
72
|
+
return error_type == "FileNotFoundError"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _create_pip_error_info(error_type: str, error_message: str, traceback_lines: List[str]) -> Dict[str, Any]:
|
|
76
|
+
"""Create custom error info for pip-related errors"""
|
|
77
|
+
|
|
78
|
+
# Extract package name if possible
|
|
79
|
+
package_name = _extract_package_name_from_pip_command(error_message, traceback_lines)
|
|
80
|
+
|
|
81
|
+
suggestions = []
|
|
82
|
+
enhanced_message = error_message
|
|
83
|
+
|
|
84
|
+
# Handle direct pip command syntax errors (most common case)
|
|
85
|
+
if "SyntaxError" in error_type and "pip install" in " ".join(traceback_lines):
|
|
86
|
+
enhanced_message = "Cannot run pip commands directly in Python code"
|
|
87
|
+
if package_name:
|
|
88
|
+
suggestions.extend([
|
|
89
|
+
f"Use shell command instead: !pip install {package_name}",
|
|
90
|
+
f"Or use subprocess: subprocess.run(['pip', 'install', '{package_name}'])",
|
|
91
|
+
"Note: pip commands need to be run in shell, not Python"
|
|
92
|
+
])
|
|
93
|
+
else:
|
|
94
|
+
suggestions.extend([
|
|
95
|
+
"Use shell command instead: !pip install <package_name>",
|
|
96
|
+
"Or use subprocess: subprocess.run(['pip', 'install', '<package_name>'])",
|
|
97
|
+
"Note: pip commands need to be run in shell, not Python"
|
|
98
|
+
])
|
|
99
|
+
elif "ModuleNotFoundError" in error_type and package_name:
|
|
100
|
+
enhanced_message = f"Package '{package_name}' is not installed"
|
|
101
|
+
suggestions.extend([
|
|
102
|
+
f"Install the package: !pip install {package_name}",
|
|
103
|
+
f"Or use subprocess: subprocess.run(['pip', 'install', '{package_name}'])",
|
|
104
|
+
"Check if the package name is spelled correctly",
|
|
105
|
+
"Verify the package exists on PyPI: https://pypi.org/"
|
|
106
|
+
])
|
|
107
|
+
elif "pip install" in error_message.lower():
|
|
108
|
+
suggestions.extend([
|
|
109
|
+
"Try using shell command: !pip install <package_name>",
|
|
110
|
+
"Check your internet connection",
|
|
111
|
+
"Verify the package name exists on PyPI",
|
|
112
|
+
"Try upgrading pip: !pip install --upgrade pip"
|
|
113
|
+
])
|
|
114
|
+
elif "No module named 'pip'" in error_message:
|
|
115
|
+
enhanced_message = "pip is not installed or not found in your Python environment"
|
|
116
|
+
suggestions.extend([
|
|
117
|
+
"Reinstall pip: python -m ensurepip --upgrade",
|
|
118
|
+
"Or download get-pip.py and run: python get-pip.py",
|
|
119
|
+
"Check if you're using the correct Python environment"
|
|
120
|
+
])
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
"ename": "PipError",
|
|
124
|
+
"evalue": enhanced_message,
|
|
125
|
+
"suggestions": suggestions,
|
|
126
|
+
"traceback": _format_traceback(traceback_lines),
|
|
127
|
+
"error_type": "pip_error"
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _create_import_error_info(error_type: str, error_message: str, traceback_lines: List[str]) -> Dict[str, Any]:
|
|
132
|
+
"""Create enhanced error info for import errors"""
|
|
133
|
+
|
|
134
|
+
# Extract module name
|
|
135
|
+
module_name = _extract_module_name(error_message)
|
|
136
|
+
|
|
137
|
+
suggestions = []
|
|
138
|
+
if module_name:
|
|
139
|
+
suggestions.extend([
|
|
140
|
+
f"Install the missing package: !pip install {module_name}",
|
|
141
|
+
"Check if the module name is spelled correctly",
|
|
142
|
+
"Verify the module is in your Python path",
|
|
143
|
+
f"Search for the correct package name: https://pypi.org/search/?q={module_name}"
|
|
144
|
+
])
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
"ename": error_type,
|
|
148
|
+
"evalue": error_message,
|
|
149
|
+
"suggestions": suggestions,
|
|
150
|
+
"traceback": _format_traceback(traceback_lines),
|
|
151
|
+
"error_type": "import_error"
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _create_file_error_info(error_type: str, error_message: str, traceback_lines: List[str]) -> Dict[str, Any]:
|
|
156
|
+
"""Create enhanced error info for file errors"""
|
|
157
|
+
|
|
158
|
+
# Extract file path if possible
|
|
159
|
+
file_path = _extract_file_path(error_message)
|
|
160
|
+
|
|
161
|
+
suggestions = [
|
|
162
|
+
"Check if the file path is correct",
|
|
163
|
+
"Verify the file exists in the specified location",
|
|
164
|
+
"Check file permissions",
|
|
165
|
+
"Use absolute path instead of relative path"
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
if file_path:
|
|
169
|
+
suggestions.insert(0, f"File not found: {file_path}")
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
"ename": error_type,
|
|
173
|
+
"evalue": error_message,
|
|
174
|
+
"suggestions": suggestions,
|
|
175
|
+
"traceback": _format_traceback(traceback_lines),
|
|
176
|
+
"error_type": "file_error"
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _create_generic_enhanced_error(error_type: str, error_message: str, traceback_lines: List[str]) -> Dict[str, Any]:
|
|
181
|
+
"""Create enhanced error info for generic errors"""
|
|
182
|
+
|
|
183
|
+
suggestions = _generate_generic_suggestions(error_type, error_message)
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
"ename": error_type,
|
|
187
|
+
"evalue": error_message,
|
|
188
|
+
"suggestions": suggestions,
|
|
189
|
+
"traceback": _format_traceback(traceback_lines),
|
|
190
|
+
"error_type": "generic_error"
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _extract_package_name(error_message: str, traceback_lines: List[str]) -> str:
|
|
195
|
+
"""Extract package name from error message or traceback"""
|
|
196
|
+
# Try to find module name in "No module named 'xyz'" pattern
|
|
197
|
+
match = re.search(r"No module named ['\"]([^'\"]+)['\"]", error_message)
|
|
198
|
+
if match:
|
|
199
|
+
return match.group(1)
|
|
200
|
+
|
|
201
|
+
# Try to find in traceback
|
|
202
|
+
combined_text = " ".join(traceback_lines)
|
|
203
|
+
match = re.search(r"import\s+(\w+)", combined_text)
|
|
204
|
+
if match:
|
|
205
|
+
return match.group(1)
|
|
206
|
+
|
|
207
|
+
return ""
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _extract_package_name_from_pip_command(error_message: str, traceback_lines: List[str]) -> str:
|
|
211
|
+
"""Extract package name from pip install command in traceback"""
|
|
212
|
+
combined_text = error_message + " " + " ".join(traceback_lines)
|
|
213
|
+
|
|
214
|
+
# Look for "pip install package-name" pattern
|
|
215
|
+
match = re.search(r"pip\s+install\s+([\w-]+)", combined_text)
|
|
216
|
+
if match:
|
|
217
|
+
return match.group(1)
|
|
218
|
+
|
|
219
|
+
# Fallback to regular package name extraction
|
|
220
|
+
return _extract_package_name(error_message, traceback_lines)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _extract_module_name(error_message: str) -> str:
|
|
224
|
+
"""Extract module name from import error message"""
|
|
225
|
+
match = re.search(r"No module named ['\"]([^'\"]+)['\"]", error_message)
|
|
226
|
+
return match.group(1) if match else ""
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _extract_file_path(error_message: str) -> str:
|
|
230
|
+
"""Extract file path from file error message"""
|
|
231
|
+
# Try to find file path in brackets or quotes
|
|
232
|
+
patterns = [
|
|
233
|
+
r"\[Errno \d+\] .+?: '([^']+)'",
|
|
234
|
+
r'"([^"]+)".*not found',
|
|
235
|
+
r"'([^']+)'.*not found"
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
for pattern in patterns:
|
|
239
|
+
match = re.search(pattern, error_message)
|
|
240
|
+
if match:
|
|
241
|
+
return match.group(1)
|
|
242
|
+
|
|
243
|
+
return ""
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _generate_generic_suggestions(error_type: str, error_message: str) -> List[str]:
|
|
247
|
+
"""Generate helpful suggestions for generic errors"""
|
|
248
|
+
suggestions = []
|
|
249
|
+
|
|
250
|
+
if error_type == "SyntaxError":
|
|
251
|
+
suggestions.extend([
|
|
252
|
+
"Check for missing parentheses, brackets, or quotes",
|
|
253
|
+
"Verify proper indentation",
|
|
254
|
+
"Look for typos in keywords or variable names"
|
|
255
|
+
])
|
|
256
|
+
elif error_type == "NameError":
|
|
257
|
+
suggestions.extend([
|
|
258
|
+
"Check if the variable is defined before use",
|
|
259
|
+
"Verify variable name spelling",
|
|
260
|
+
"Make sure to import required modules"
|
|
261
|
+
])
|
|
262
|
+
elif error_type == "TypeError":
|
|
263
|
+
suggestions.extend([
|
|
264
|
+
"Check function arguments and their types",
|
|
265
|
+
"Verify object methods and attributes",
|
|
266
|
+
"Check if you're calling a function correctly"
|
|
267
|
+
])
|
|
268
|
+
elif error_type == "ValueError":
|
|
269
|
+
suggestions.extend([
|
|
270
|
+
"Check input values and their formats",
|
|
271
|
+
"Verify numeric conversions",
|
|
272
|
+
"Check if values are within expected ranges"
|
|
273
|
+
])
|
|
274
|
+
elif error_type == "KeyError":
|
|
275
|
+
suggestions.extend([
|
|
276
|
+
"Check if the key exists in the dictionary",
|
|
277
|
+
"Use .get() method for safer key access",
|
|
278
|
+
"Verify the key spelling and type"
|
|
279
|
+
])
|
|
280
|
+
elif error_type == "IndexError":
|
|
281
|
+
suggestions.extend([
|
|
282
|
+
"Check list/array bounds",
|
|
283
|
+
"Verify the index is within range",
|
|
284
|
+
"Check if the list is empty"
|
|
285
|
+
])
|
|
286
|
+
else:
|
|
287
|
+
suggestions.extend([
|
|
288
|
+
"Check the error message for specific details",
|
|
289
|
+
"Look at the line number in the traceback",
|
|
290
|
+
"Search online for this specific error type"
|
|
291
|
+
])
|
|
292
|
+
|
|
293
|
+
return suggestions
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _format_traceback(traceback_lines: List[str]) -> List[str]:
|
|
297
|
+
"""Format traceback lines for better readability"""
|
|
298
|
+
if not traceback_lines:
|
|
299
|
+
return []
|
|
300
|
+
|
|
301
|
+
# Remove empty lines and clean up formatting
|
|
302
|
+
formatted_lines = []
|
|
303
|
+
for line in traceback_lines:
|
|
304
|
+
if line.strip():
|
|
305
|
+
formatted_lines.append(line.rstrip())
|
|
306
|
+
|
|
307
|
+
# Limit to last 15 lines for readability
|
|
308
|
+
if len(formatted_lines) > 15:
|
|
309
|
+
formatted_lines = ["... (traceback truncated) ..."] + formatted_lines[-15:]
|
|
310
|
+
|
|
311
|
+
return formatted_lines
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
class ErrorUtils:
|
|
315
|
+
"""Helper class to generate enhanced error payloads for the frontend."""
|
|
316
|
+
|
|
317
|
+
def format_exception(self, exception: Exception) -> Dict[str, Any]:
|
|
318
|
+
"""Return enhanced error information for the given exception."""
|
|
319
|
+
|
|
320
|
+
formatted_traceback = traceback.format_exception(type(exception), exception, exception.__traceback__)
|
|
321
|
+
cleaned_traceback = [line.rstrip("\n") for line in formatted_traceback]
|
|
322
|
+
return create_enhanced_error_info(exception, cleaned_traceback)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Notebook utility functions for cell processing."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def coerce_cell_source(value):
|
|
5
|
+
"""
|
|
6
|
+
Coerce various cell source formats to a string.
|
|
7
|
+
|
|
8
|
+
Handles:
|
|
9
|
+
- None → empty string
|
|
10
|
+
- str → unchanged
|
|
11
|
+
- bytes/bytearray → decoded to UTF-8
|
|
12
|
+
- list → joined into single string
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
value: Cell source in various formats
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
String representation of cell source
|
|
19
|
+
"""
|
|
20
|
+
if value is None:
|
|
21
|
+
return ''
|
|
22
|
+
if isinstance(value, str):
|
|
23
|
+
return value
|
|
24
|
+
if isinstance(value, (bytes, bytearray)):
|
|
25
|
+
try:
|
|
26
|
+
return value.decode('utf-8') # type: ignore[arg-type]
|
|
27
|
+
except Exception:
|
|
28
|
+
return value.decode('utf-8', errors='ignore') # type: ignore[arg-type]
|
|
29
|
+
if isinstance(value, list):
|
|
30
|
+
parts = []
|
|
31
|
+
for item in value:
|
|
32
|
+
if item is None:
|
|
33
|
+
continue
|
|
34
|
+
if isinstance(item, str):
|
|
35
|
+
parts.append(item)
|
|
36
|
+
elif isinstance(item, (bytes, bytearray)):
|
|
37
|
+
try:
|
|
38
|
+
parts.append(item.decode('utf-8')) # type: ignore[arg-type]
|
|
39
|
+
except Exception:
|
|
40
|
+
parts.append(item.decode('utf-8', errors='ignore')) # type: ignore[arg-type]
|
|
41
|
+
else:
|
|
42
|
+
parts.append(str(item))
|
|
43
|
+
return ''.join(parts)
|
|
44
|
+
return str(value)
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
import platform
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PythonEnvironmentDetector:
|
|
10
|
+
"""Detects Python environments (system Python and conda)"""
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self.system = platform.system().lower()
|
|
14
|
+
|
|
15
|
+
def detect_all_environments(self) -> list[dict[str, str]]:
|
|
16
|
+
"""Detect all Python environments on the system"""
|
|
17
|
+
environments = []
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
# 1. Conda environments (check first for proper naming)
|
|
21
|
+
environments.extend(self._detect_conda_environments())
|
|
22
|
+
except Exception as e:
|
|
23
|
+
print(f"Warning: Conda detection failed: {e}")
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
# 2. System Python installations
|
|
27
|
+
environments.extend(self._detect_system_python())
|
|
28
|
+
except Exception as e:
|
|
29
|
+
print(f"Warning: System Python detection failed: {e}")
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
# 3. Check for venv in current directory
|
|
33
|
+
environments.extend(self._detect_local_venv())
|
|
34
|
+
except Exception as e:
|
|
35
|
+
print(f"Warning: Local venv detection failed: {e}")
|
|
36
|
+
|
|
37
|
+
# Remove duplicates based on path (keep first occurrence)
|
|
38
|
+
seen_paths = set()
|
|
39
|
+
unique_environments = []
|
|
40
|
+
for env in environments:
|
|
41
|
+
if env['path'] not in seen_paths:
|
|
42
|
+
seen_paths.add(env['path'])
|
|
43
|
+
unique_environments.append(env)
|
|
44
|
+
|
|
45
|
+
return sorted(unique_environments, key=lambda x: x['name'])
|
|
46
|
+
|
|
47
|
+
def _detect_system_python(self) -> list[dict[str, str]]:
|
|
48
|
+
"""Detect system Python installations"""
|
|
49
|
+
environments = []
|
|
50
|
+
|
|
51
|
+
# Common Python executable names
|
|
52
|
+
python_names = ['python3', 'python']
|
|
53
|
+
|
|
54
|
+
if self.system == 'windows':
|
|
55
|
+
python_names.extend(['py', 'python.exe'])
|
|
56
|
+
|
|
57
|
+
for python_name in python_names:
|
|
58
|
+
try:
|
|
59
|
+
cmd = 'where' if self.system == 'windows' else 'which'
|
|
60
|
+
result = subprocess.run([cmd, python_name],
|
|
61
|
+
capture_output=True, text=True, timeout=5)
|
|
62
|
+
|
|
63
|
+
if result.returncode == 0:
|
|
64
|
+
python_path = result.stdout.strip().split('\n')[0]
|
|
65
|
+
version = self._get_python_version(python_path)
|
|
66
|
+
|
|
67
|
+
if version:
|
|
68
|
+
environments.append({
|
|
69
|
+
'name': f'System Python ({python_name})',
|
|
70
|
+
'path': python_path,
|
|
71
|
+
'version': version,
|
|
72
|
+
'type': 'system',
|
|
73
|
+
'active': python_path == sys.executable
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
return environments
|
|
80
|
+
|
|
81
|
+
def _detect_conda_environments(self) -> list[dict[str, str]]:
|
|
82
|
+
"""Detect Conda/Mamba environments"""
|
|
83
|
+
environments = []
|
|
84
|
+
|
|
85
|
+
# Try conda first, then mamba
|
|
86
|
+
for cmd in ['conda', 'mamba']:
|
|
87
|
+
try:
|
|
88
|
+
result = subprocess.run([cmd, 'env', 'list', '--json'],
|
|
89
|
+
capture_output=True, text=True, timeout=10)
|
|
90
|
+
|
|
91
|
+
if result.returncode == 0:
|
|
92
|
+
data = json.loads(result.stdout)
|
|
93
|
+
root_prefix = data.get('root_prefix', '')
|
|
94
|
+
|
|
95
|
+
for env_path in data.get('envs', []):
|
|
96
|
+
# Find python in conda env
|
|
97
|
+
env_path_obj = Path(env_path)
|
|
98
|
+
if self.system == 'windows':
|
|
99
|
+
python_path = env_path_obj / 'python.exe'
|
|
100
|
+
else:
|
|
101
|
+
python_path = env_path_obj / 'bin' / 'python'
|
|
102
|
+
|
|
103
|
+
if python_path.exists() and python_path.is_file():
|
|
104
|
+
# Check if this is the base/root environment
|
|
105
|
+
if env_path == root_prefix:
|
|
106
|
+
env_name = f'{cmd} (base)'
|
|
107
|
+
else:
|
|
108
|
+
env_name = os.path.basename(env_path)
|
|
109
|
+
|
|
110
|
+
version = self._get_python_version(str(python_path))
|
|
111
|
+
if version:
|
|
112
|
+
environments.append({
|
|
113
|
+
'name': env_name,
|
|
114
|
+
'path': str(python_path),
|
|
115
|
+
'version': version,
|
|
116
|
+
'type': 'conda',
|
|
117
|
+
'active': str(python_path) == sys.executable
|
|
118
|
+
})
|
|
119
|
+
break
|
|
120
|
+
|
|
121
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, json.JSONDecodeError):
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
return environments
|
|
125
|
+
|
|
126
|
+
def _detect_local_venv(self) -> list[dict[str, str]]:
|
|
127
|
+
"""Detect virtual environments in current directory only"""
|
|
128
|
+
environments = []
|
|
129
|
+
|
|
130
|
+
# Only check common venv names in current directory
|
|
131
|
+
venv_names = ['.venv', 'venv']
|
|
132
|
+
|
|
133
|
+
for venv_name in venv_names:
|
|
134
|
+
venv_path = Path.cwd() / venv_name
|
|
135
|
+
if venv_path.exists() and venv_path.is_dir():
|
|
136
|
+
# Find python executable
|
|
137
|
+
if self.system == 'windows':
|
|
138
|
+
python_path = venv_path / 'Scripts' / 'python.exe'
|
|
139
|
+
else:
|
|
140
|
+
python_path = venv_path / 'bin' / 'python'
|
|
141
|
+
|
|
142
|
+
if python_path.exists() and python_path.is_file():
|
|
143
|
+
version = self._get_python_version(str(python_path))
|
|
144
|
+
if version:
|
|
145
|
+
environments.append({
|
|
146
|
+
'name': venv_name,
|
|
147
|
+
'path': str(python_path),
|
|
148
|
+
'version': version,
|
|
149
|
+
'type': 'venv',
|
|
150
|
+
'active': str(python_path) == sys.executable
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
return environments
|
|
154
|
+
|
|
155
|
+
def _get_python_version(self, python_path: str) -> str | None:
|
|
156
|
+
"""Get Python version from executable"""
|
|
157
|
+
try:
|
|
158
|
+
result = subprocess.run([python_path, '--version'],
|
|
159
|
+
capture_output=True, text=True, timeout=5)
|
|
160
|
+
|
|
161
|
+
if result.returncode == 0:
|
|
162
|
+
# Parse "Python 3.11.4" -> "3.11.4"
|
|
163
|
+
version_line = result.stdout.strip()
|
|
164
|
+
if version_line.startswith('Python '):
|
|
165
|
+
return version_line[7:] # Remove "Python "
|
|
166
|
+
|
|
167
|
+
except (subprocess.TimeoutExpired, FileNotFoundError):
|
|
168
|
+
pass
|
|
169
|
+
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
def get_current_environment(self) -> dict[str, str]:
|
|
173
|
+
"""Get information about the currently active Python environment"""
|
|
174
|
+
return {
|
|
175
|
+
'name': 'Current Python',
|
|
176
|
+
'path': sys.executable,
|
|
177
|
+
'version': f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
|
|
178
|
+
'type': 'current',
|
|
179
|
+
'active': True
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# Example usage
|
|
184
|
+
if __name__ == "__main__":
|
|
185
|
+
detector = PythonEnvironmentDetector()
|
|
186
|
+
|
|
187
|
+
print("Detecting Python environments...")
|
|
188
|
+
environments = detector.detect_all_environments()
|
|
189
|
+
|
|
190
|
+
print(f"\nFound {len(environments)} Python environments:")
|
|
191
|
+
print("-" * 60)
|
|
192
|
+
|
|
193
|
+
for env in environments:
|
|
194
|
+
status = "ACTIVE" if env['active'] else ""
|
|
195
|
+
print(f"{env['name']:<25} Python {env['version']:<8} {env['type']:<8} {status}")
|
|
196
|
+
print(f"{'':25} {env['path']}")
|
|
197
|
+
print()
|