slurmray 6.0.4__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.
Potentially problematic release.
This version of slurmray might be problematic. Click here for more details.
- slurmray/RayLauncher.py +1019 -0
- slurmray/__init__.py +0 -0
- slurmray/__main__.py +5 -0
- slurmray/assets/cleanup_old_projects.py +171 -0
- slurmray/assets/sbatch_template.sh +67 -0
- slurmray/assets/slurmray_server.sh +145 -0
- slurmray/assets/slurmray_server_template.py +28 -0
- slurmray/assets/spython_template.py +113 -0
- slurmray/backend/__init__.py +0 -0
- slurmray/backend/base.py +1040 -0
- slurmray/backend/desi.py +856 -0
- slurmray/backend/local.py +124 -0
- slurmray/backend/remote.py +191 -0
- slurmray/backend/slurm.py +1234 -0
- slurmray/cli.py +904 -0
- slurmray/detection.py +1 -0
- slurmray/file_sync.py +276 -0
- slurmray/scanner.py +441 -0
- slurmray/utils.py +359 -0
- slurmray-6.0.4.dist-info/LICENSE +201 -0
- slurmray-6.0.4.dist-info/METADATA +85 -0
- slurmray-6.0.4.dist-info/RECORD +24 -0
- slurmray-6.0.4.dist-info/WHEEL +4 -0
- slurmray-6.0.4.dist-info/entry_points.txt +3 -0
slurmray/scanner.py
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import ast
|
|
3
|
+
import sys
|
|
4
|
+
import pkgutil
|
|
5
|
+
import importlib.util
|
|
6
|
+
import site
|
|
7
|
+
from typing import List, Set, Dict, Tuple
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ProjectScanner:
|
|
12
|
+
"""
|
|
13
|
+
Scans the project for local imports and potential dynamic loading issues.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, project_root: str, logger: logging.Logger = None):
|
|
17
|
+
self.project_root = os.path.abspath(project_root)
|
|
18
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
19
|
+
self.local_modules = set()
|
|
20
|
+
self.dynamic_imports_warnings = []
|
|
21
|
+
|
|
22
|
+
# Standard library modules to ignore
|
|
23
|
+
self.stdlib_modules = self._get_stdlib_modules()
|
|
24
|
+
|
|
25
|
+
def _get_stdlib_modules(self) -> Set[str]:
|
|
26
|
+
"""Get a set of standard library module names."""
|
|
27
|
+
stdlib = set(sys.builtin_module_names)
|
|
28
|
+
|
|
29
|
+
# Add standard library modules from dist-packages/lib-dynload
|
|
30
|
+
for module in pkgutil.iter_modules():
|
|
31
|
+
# This is a heuristic, might include some site-packages if not careful
|
|
32
|
+
# But we primarily filter by checking if file exists locally
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
return stdlib
|
|
36
|
+
|
|
37
|
+
def _is_system_or_venv_file(self, file_path: str) -> bool:
|
|
38
|
+
"""
|
|
39
|
+
Check if a file belongs to system or venv (installed package or stdlib).
|
|
40
|
+
Uses site.getsitepackages() and sys.prefix for robust detection.
|
|
41
|
+
"""
|
|
42
|
+
file_abs = os.path.abspath(file_path)
|
|
43
|
+
|
|
44
|
+
# 1. Check against site-packages directories (primary method for installed packages)
|
|
45
|
+
site_packages_dirs = site.getsitepackages()
|
|
46
|
+
if hasattr(site, "getusersitepackages"):
|
|
47
|
+
user_site = site.getusersitepackages()
|
|
48
|
+
if user_site:
|
|
49
|
+
site_packages_dirs = list(site_packages_dirs) + [user_site]
|
|
50
|
+
|
|
51
|
+
for site_pkg_dir in site_packages_dirs:
|
|
52
|
+
if file_abs.startswith(os.path.abspath(site_pkg_dir)):
|
|
53
|
+
return True
|
|
54
|
+
|
|
55
|
+
# 2. Check against sys.prefix/base_prefix (for standard library and venv files)
|
|
56
|
+
# We verify subdirectories like 'lib', 'include' to avoid matching the project root
|
|
57
|
+
# if the project is located directly in the prefix (unlikely but possible)
|
|
58
|
+
prefixes = [sys.prefix]
|
|
59
|
+
if hasattr(sys, "base_prefix") and sys.base_prefix != sys.prefix:
|
|
60
|
+
prefixes.append(sys.base_prefix)
|
|
61
|
+
|
|
62
|
+
# Common system directories to exclude
|
|
63
|
+
system_dirs = ["lib", "include", "bin", "Scripts", "Library", "DLLs", "Lib"]
|
|
64
|
+
|
|
65
|
+
for prefix in prefixes:
|
|
66
|
+
prefix_abs = os.path.abspath(prefix)
|
|
67
|
+
if file_abs.startswith(prefix_abs):
|
|
68
|
+
# Check if it's in a system subdirectory
|
|
69
|
+
for sys_dir in system_dirs:
|
|
70
|
+
sys_dir_abs = os.path.join(prefix_abs, sys_dir)
|
|
71
|
+
if file_abs.startswith(sys_dir_abs):
|
|
72
|
+
return True
|
|
73
|
+
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
def is_local_file(self, module_name: str) -> Tuple[bool, str]:
|
|
77
|
+
"""
|
|
78
|
+
Check if a module corresponds to a local file in the project.
|
|
79
|
+
Uses Python's import system to resolve the module location robustly.
|
|
80
|
+
Returns (is_local, file_path).
|
|
81
|
+
"""
|
|
82
|
+
try:
|
|
83
|
+
# Use Python's import system to find where the module actually is
|
|
84
|
+
spec = importlib.util.find_spec(module_name)
|
|
85
|
+
|
|
86
|
+
if spec is None:
|
|
87
|
+
# Module not found
|
|
88
|
+
return False, None
|
|
89
|
+
|
|
90
|
+
# Check if module has an origin (file location)
|
|
91
|
+
if spec.origin is None:
|
|
92
|
+
# Built-in module or namespace package without origin
|
|
93
|
+
return False, None
|
|
94
|
+
|
|
95
|
+
# Check if origin is a file (not a directory for namespace packages)
|
|
96
|
+
if not spec.origin.endswith(".py"):
|
|
97
|
+
# Could be a namespace package or compiled module
|
|
98
|
+
# For namespace packages, check if it's in our project
|
|
99
|
+
if (
|
|
100
|
+
hasattr(spec, "submodule_search_locations")
|
|
101
|
+
and spec.submodule_search_locations
|
|
102
|
+
):
|
|
103
|
+
for location in spec.submodule_search_locations:
|
|
104
|
+
location_abs = os.path.abspath(location)
|
|
105
|
+
if location_abs.startswith(self.project_root):
|
|
106
|
+
# It's a namespace package in our project
|
|
107
|
+
rel_path = os.path.relpath(location_abs, self.project_root)
|
|
108
|
+
return True, rel_path
|
|
109
|
+
return False, None
|
|
110
|
+
|
|
111
|
+
# Get absolute path of the module file
|
|
112
|
+
module_file = os.path.abspath(spec.origin)
|
|
113
|
+
|
|
114
|
+
# Check if it's a system or venv file
|
|
115
|
+
if self._is_system_or_venv_file(module_file):
|
|
116
|
+
return False, None
|
|
117
|
+
|
|
118
|
+
# Check if it's within project root
|
|
119
|
+
if not module_file.startswith(self.project_root):
|
|
120
|
+
return False, None
|
|
121
|
+
|
|
122
|
+
# Get relative path from project root
|
|
123
|
+
rel_path = os.path.relpath(module_file, self.project_root)
|
|
124
|
+
|
|
125
|
+
# For packages, we might want to return the directory instead of __init__.py
|
|
126
|
+
if rel_path.endswith("__init__.py"):
|
|
127
|
+
# Return the package directory
|
|
128
|
+
rel_path = os.path.dirname(rel_path)
|
|
129
|
+
# If it's empty after removing __init__.py, it's the root package
|
|
130
|
+
if not rel_path:
|
|
131
|
+
return False, None
|
|
132
|
+
|
|
133
|
+
return True, rel_path
|
|
134
|
+
|
|
135
|
+
except (ImportError, ValueError, AttributeError) as e:
|
|
136
|
+
# Module resolution failed
|
|
137
|
+
if self.logger:
|
|
138
|
+
self.logger.debug(f"Could not resolve module {module_name}: {e}")
|
|
139
|
+
return False, None
|
|
140
|
+
|
|
141
|
+
def scan_file(self, file_path: str) -> Set[str]:
|
|
142
|
+
"""
|
|
143
|
+
Scan a python file for imports and dynamic loading patterns.
|
|
144
|
+
Returns a set of detected local dependencies (relative paths from project root).
|
|
145
|
+
"""
|
|
146
|
+
dependencies = set()
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
150
|
+
content = f.read()
|
|
151
|
+
|
|
152
|
+
tree = ast.parse(content, filename=file_path)
|
|
153
|
+
|
|
154
|
+
# Get directory of current file for relative imports
|
|
155
|
+
file_dir = os.path.dirname(os.path.abspath(file_path))
|
|
156
|
+
|
|
157
|
+
for node in ast.walk(tree):
|
|
158
|
+
# 1. Static Imports
|
|
159
|
+
if isinstance(node, ast.Import):
|
|
160
|
+
for alias in node.names:
|
|
161
|
+
is_local, path = self.is_local_file(alias.name)
|
|
162
|
+
if is_local:
|
|
163
|
+
dependencies.add(path)
|
|
164
|
+
|
|
165
|
+
elif isinstance(node, ast.ImportFrom):
|
|
166
|
+
if node.level > 0:
|
|
167
|
+
# Relative import (from . import ... or from .. import ...)
|
|
168
|
+
# Resolve relative import based on current file location
|
|
169
|
+
# Calculate relative path from current file
|
|
170
|
+
current_rel = os.path.relpath(file_dir, self.project_root)
|
|
171
|
+
parts = (
|
|
172
|
+
current_rel.split(os.sep) if current_rel != "." else []
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Go up 'level' directories
|
|
176
|
+
if node.level <= len(parts):
|
|
177
|
+
parent_parts = parts[: len(parts) - (node.level - 1)]
|
|
178
|
+
module_parts = node.module.split(".") if node.module else []
|
|
179
|
+
|
|
180
|
+
# We want to check:
|
|
181
|
+
# 1. The module itself (if present)
|
|
182
|
+
# 2. Each name imported from it
|
|
183
|
+
|
|
184
|
+
to_check = []
|
|
185
|
+
if node.module:
|
|
186
|
+
to_check.append(parent_parts + module_parts)
|
|
187
|
+
|
|
188
|
+
for alias in node.names:
|
|
189
|
+
to_check.append(parent_parts + module_parts + [alias.name])
|
|
190
|
+
|
|
191
|
+
for parts_list in to_check:
|
|
192
|
+
rel_path = os.path.join(*parts_list) if parts_list else ""
|
|
193
|
+
if rel_path:
|
|
194
|
+
# Try as file
|
|
195
|
+
abs_path = os.path.join(
|
|
196
|
+
self.project_root, rel_path + ".py"
|
|
197
|
+
)
|
|
198
|
+
if os.path.exists(abs_path):
|
|
199
|
+
dependencies.add(rel_path + ".py")
|
|
200
|
+
else:
|
|
201
|
+
# Try as package
|
|
202
|
+
abs_path = os.path.join(
|
|
203
|
+
self.project_root, rel_path, "__init__.py"
|
|
204
|
+
)
|
|
205
|
+
if os.path.exists(abs_path):
|
|
206
|
+
dependencies.add(rel_path)
|
|
207
|
+
elif node.module:
|
|
208
|
+
# from module import ... (absolute import)
|
|
209
|
+
# Check the module itself
|
|
210
|
+
is_local, path = self.is_local_file(node.module)
|
|
211
|
+
if is_local:
|
|
212
|
+
dependencies.add(path)
|
|
213
|
+
|
|
214
|
+
# Also check each name in case they are submodules
|
|
215
|
+
for alias in node.names:
|
|
216
|
+
full_name = f"{node.module}.{alias.name}"
|
|
217
|
+
is_local_sub, sub_path = self.is_local_file(full_name)
|
|
218
|
+
if is_local_sub:
|
|
219
|
+
dependencies.add(sub_path)
|
|
220
|
+
|
|
221
|
+
# 2. Dynamic Imports & File Operations Warnings
|
|
222
|
+
elif isinstance(node, ast.Call):
|
|
223
|
+
if isinstance(node.func, ast.Attribute):
|
|
224
|
+
# importlib.import_module(...)
|
|
225
|
+
if (
|
|
226
|
+
node.func.attr == "import_module"
|
|
227
|
+
and isinstance(node.func.value, ast.Name)
|
|
228
|
+
and node.func.value.id == "importlib"
|
|
229
|
+
):
|
|
230
|
+
self._add_warning(
|
|
231
|
+
file_path,
|
|
232
|
+
node.lineno,
|
|
233
|
+
"importlib.import_module usage detected",
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
elif isinstance(node.func, ast.Name):
|
|
237
|
+
# __import__(...)
|
|
238
|
+
if node.func.id == "__import__":
|
|
239
|
+
self._add_warning(
|
|
240
|
+
file_path, node.lineno, "__import__ usage detected"
|
|
241
|
+
)
|
|
242
|
+
# open(...)
|
|
243
|
+
elif node.func.id == "open":
|
|
244
|
+
# Check if argument is a string literal
|
|
245
|
+
if (
|
|
246
|
+
node.args
|
|
247
|
+
and isinstance(node.args[0], ast.Constant)
|
|
248
|
+
and isinstance(node.args[0].value, str)
|
|
249
|
+
):
|
|
250
|
+
path_arg = node.args[0].value
|
|
251
|
+
# Warn if it looks like a path to a file that might be missing
|
|
252
|
+
if not os.path.isabs(
|
|
253
|
+
path_arg
|
|
254
|
+
) and not path_arg.startswith("/"):
|
|
255
|
+
self._add_warning(
|
|
256
|
+
file_path,
|
|
257
|
+
node.lineno,
|
|
258
|
+
f"open('{path_arg}') detected. Ensure this file is in 'files' list if needed.",
|
|
259
|
+
)
|
|
260
|
+
else:
|
|
261
|
+
self._add_warning(
|
|
262
|
+
file_path,
|
|
263
|
+
node.lineno,
|
|
264
|
+
"open() with dynamic path detected",
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
except Exception as e:
|
|
268
|
+
self.logger.debug(f"Failed to scan {file_path}: {e}")
|
|
269
|
+
|
|
270
|
+
return dependencies
|
|
271
|
+
|
|
272
|
+
def _add_warning(self, file_path: str, lineno: int, message: str):
|
|
273
|
+
file_abs = os.path.abspath(file_path)
|
|
274
|
+
|
|
275
|
+
# Filter out warnings from installed packages or system files
|
|
276
|
+
if self._is_system_or_venv_file(file_abs):
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
# Check if file is within project root
|
|
280
|
+
# If file is outside project root and not in site-packages (already checked), ignore it
|
|
281
|
+
try:
|
|
282
|
+
# If file is outside project root, relpath will start with '../'
|
|
283
|
+
rel_path = os.path.relpath(file_abs, self.project_root)
|
|
284
|
+
if rel_path.startswith("../"):
|
|
285
|
+
# File is outside project root → ignore
|
|
286
|
+
return
|
|
287
|
+
except ValueError:
|
|
288
|
+
# If files are on different drives (Windows), relpath raises ValueError
|
|
289
|
+
# In this case, check if file_abs starts with project_root
|
|
290
|
+
if not file_abs.startswith(self.project_root):
|
|
291
|
+
# File is outside project root → ignore
|
|
292
|
+
return
|
|
293
|
+
rel_path = os.path.relpath(file_abs, self.project_root)
|
|
294
|
+
|
|
295
|
+
# Filter out warnings from slurmray's own code and test files
|
|
296
|
+
# Use __file__ to get the absolute path of slurmray package
|
|
297
|
+
try:
|
|
298
|
+
import slurmray
|
|
299
|
+
|
|
300
|
+
slurmray_dir = os.path.dirname(os.path.abspath(slurmray.__file__))
|
|
301
|
+
|
|
302
|
+
# Check if file is in slurmray directory or its subdirectories
|
|
303
|
+
if file_abs.startswith(slurmray_dir):
|
|
304
|
+
return # Skip warnings from slurmray framework code
|
|
305
|
+
except (AttributeError, ImportError):
|
|
306
|
+
# Fallback: if we can't determine slurmray location, use path prefix check
|
|
307
|
+
if rel_path.startswith("slurmray/"):
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
# Also filter out common non-user directories
|
|
311
|
+
if (
|
|
312
|
+
rel_path.startswith("tests/")
|
|
313
|
+
or rel_path.startswith(".slogs/")
|
|
314
|
+
or rel_path.startswith("old_")
|
|
315
|
+
or rel_path.startswith("debug_")
|
|
316
|
+
):
|
|
317
|
+
return # Skip warnings from test files and temporary directories
|
|
318
|
+
|
|
319
|
+
self.dynamic_imports_warnings.append(f"{rel_path}:{lineno}: {message}")
|
|
320
|
+
|
|
321
|
+
def _follow_imports_recursive(
|
|
322
|
+
self, file_path: str, visited: Set[str] = None, path_modified: bool = False
|
|
323
|
+
) -> Set[str]:
|
|
324
|
+
"""
|
|
325
|
+
Recursively follow imports starting from a file.
|
|
326
|
+
Returns set of relative paths to local dependencies.
|
|
327
|
+
"""
|
|
328
|
+
if visited is None:
|
|
329
|
+
visited = set()
|
|
330
|
+
# Ensure project_root is in sys.path for importlib to work
|
|
331
|
+
# (needed for is_local_file to resolve modules correctly)
|
|
332
|
+
project_in_path = self.project_root in sys.path
|
|
333
|
+
src_path = os.path.join(self.project_root, "src")
|
|
334
|
+
src_in_path = src_path in sys.path
|
|
335
|
+
|
|
336
|
+
if not project_in_path:
|
|
337
|
+
sys.path.insert(0, self.project_root)
|
|
338
|
+
if not src_in_path and os.path.exists(src_path):
|
|
339
|
+
sys.path.insert(0, src_path)
|
|
340
|
+
path_modified = not project_in_path or (
|
|
341
|
+
not src_in_path and os.path.exists(src_path)
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
# Normalize path
|
|
345
|
+
file_path = os.path.abspath(file_path)
|
|
346
|
+
if file_path in visited:
|
|
347
|
+
return set()
|
|
348
|
+
visited.add(file_path)
|
|
349
|
+
|
|
350
|
+
# Check if file is within project
|
|
351
|
+
if not file_path.startswith(self.project_root):
|
|
352
|
+
return set()
|
|
353
|
+
|
|
354
|
+
# Check if it's a system or venv file (double check to avoid scanning venv inside project)
|
|
355
|
+
if self._is_system_or_venv_file(file_path):
|
|
356
|
+
return set()
|
|
357
|
+
|
|
358
|
+
# Get relative path
|
|
359
|
+
try:
|
|
360
|
+
rel_path = os.path.relpath(file_path, self.project_root)
|
|
361
|
+
except ValueError:
|
|
362
|
+
return set()
|
|
363
|
+
|
|
364
|
+
dependencies = set()
|
|
365
|
+
|
|
366
|
+
try:
|
|
367
|
+
# Scan this file for imports
|
|
368
|
+
deps = self.scan_file(file_path)
|
|
369
|
+
dependencies.update(deps)
|
|
370
|
+
|
|
371
|
+
# Recursively follow each dependency
|
|
372
|
+
for dep in deps:
|
|
373
|
+
# Convert relative path to absolute
|
|
374
|
+
dep_abs = os.path.abspath(os.path.join(self.project_root, dep))
|
|
375
|
+
|
|
376
|
+
# Check if it's a file or directory
|
|
377
|
+
if os.path.isfile(dep_abs):
|
|
378
|
+
# It's a file, follow it
|
|
379
|
+
sub_deps = self._follow_imports_recursive(
|
|
380
|
+
dep_abs, visited, path_modified
|
|
381
|
+
)
|
|
382
|
+
dependencies.update(sub_deps)
|
|
383
|
+
elif os.path.isdir(dep_abs):
|
|
384
|
+
# It's a directory (package), check for __init__.py
|
|
385
|
+
init_file = os.path.join(dep_abs, "__init__.py")
|
|
386
|
+
if os.path.exists(init_file):
|
|
387
|
+
sub_deps = self._follow_imports_recursive(
|
|
388
|
+
init_file, visited, path_modified
|
|
389
|
+
)
|
|
390
|
+
dependencies.update(sub_deps)
|
|
391
|
+
|
|
392
|
+
return dependencies
|
|
393
|
+
finally:
|
|
394
|
+
# Restore sys.path only if we're the top-level call
|
|
395
|
+
if path_modified and len(visited) == 1: # Only restore on first call
|
|
396
|
+
if self.project_root in sys.path:
|
|
397
|
+
sys.path.remove(self.project_root)
|
|
398
|
+
src_path = os.path.join(self.project_root, "src")
|
|
399
|
+
if src_path in sys.path:
|
|
400
|
+
sys.path.remove(src_path)
|
|
401
|
+
|
|
402
|
+
def detect_dependencies_from_function(self, func) -> List[str]:
|
|
403
|
+
"""
|
|
404
|
+
Detect dependencies starting from a function's source file.
|
|
405
|
+
Follows imports recursively to find all local dependencies.
|
|
406
|
+
"""
|
|
407
|
+
import inspect
|
|
408
|
+
|
|
409
|
+
try:
|
|
410
|
+
# Get the file where the function is defined
|
|
411
|
+
func_file = inspect.getfile(func)
|
|
412
|
+
func_file = os.path.abspath(func_file)
|
|
413
|
+
|
|
414
|
+
# Check if function is in project
|
|
415
|
+
if not func_file.startswith(self.project_root):
|
|
416
|
+
self.logger.debug(
|
|
417
|
+
f"Function {func.__name__} is not in project root, skipping dependency detection"
|
|
418
|
+
)
|
|
419
|
+
return []
|
|
420
|
+
|
|
421
|
+
self.logger.info(
|
|
422
|
+
f"Analyzing dependencies starting from {os.path.relpath(func_file, self.project_root)}"
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
# Follow imports recursively (sys.path is managed inside _follow_imports_recursive)
|
|
426
|
+
dependencies = self._follow_imports_recursive(func_file)
|
|
427
|
+
|
|
428
|
+
# Convert to relative paths and normalize
|
|
429
|
+
result = set()
|
|
430
|
+
for dep in dependencies:
|
|
431
|
+
if dep:
|
|
432
|
+
# dep is already a relative path from scan_file
|
|
433
|
+
result.add(dep)
|
|
434
|
+
|
|
435
|
+
return list(result)
|
|
436
|
+
|
|
437
|
+
except (OSError, TypeError) as e:
|
|
438
|
+
self.logger.warning(
|
|
439
|
+
f"Could not determine source file for function {func.__name__}: {e}"
|
|
440
|
+
)
|
|
441
|
+
return []
|