siliconcompiler 0.33.1__py3-none-any.whl → 0.34.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 (59) hide show
  1. siliconcompiler/__init__.py +2 -0
  2. siliconcompiler/_metadata.py +1 -1
  3. siliconcompiler/apps/sc_issue.py +5 -3
  4. siliconcompiler/apps/sc_remote.py +0 -17
  5. siliconcompiler/apps/utils/replay.py +5 -5
  6. siliconcompiler/checklist.py +1 -1
  7. siliconcompiler/core.py +39 -48
  8. siliconcompiler/data/templates/replay/replay.sh.j2 +18 -1
  9. siliconcompiler/dependencyschema.py +392 -0
  10. siliconcompiler/design.py +664 -0
  11. siliconcompiler/flowgraph.py +32 -1
  12. siliconcompiler/metric.py +19 -0
  13. siliconcompiler/package/__init__.py +383 -223
  14. siliconcompiler/package/git.py +75 -77
  15. siliconcompiler/package/github.py +70 -97
  16. siliconcompiler/package/https.py +77 -93
  17. siliconcompiler/packageschema.py +260 -0
  18. siliconcompiler/pdk.py +2 -2
  19. siliconcompiler/record.py +57 -5
  20. siliconcompiler/remote/client.py +61 -13
  21. siliconcompiler/remote/server.py +109 -64
  22. siliconcompiler/report/dashboard/cli/board.py +1 -2
  23. siliconcompiler/scheduler/__init__.py +3 -1375
  24. siliconcompiler/scheduler/docker.py +268 -0
  25. siliconcompiler/scheduler/run_node.py +20 -19
  26. siliconcompiler/scheduler/scheduler.py +308 -0
  27. siliconcompiler/scheduler/schedulernode.py +934 -0
  28. siliconcompiler/scheduler/slurm.py +147 -163
  29. siliconcompiler/scheduler/taskscheduler.py +39 -52
  30. siliconcompiler/schema/__init__.py +3 -3
  31. siliconcompiler/schema/baseschema.py +256 -11
  32. siliconcompiler/schema/editableschema.py +4 -0
  33. siliconcompiler/schema/journal.py +210 -0
  34. siliconcompiler/schema/namedschema.py +31 -2
  35. siliconcompiler/schema/parameter.py +14 -1
  36. siliconcompiler/schema/parametervalue.py +1 -34
  37. siliconcompiler/schema/schema_cfg.py +211 -350
  38. siliconcompiler/tool.py +139 -37
  39. siliconcompiler/tools/_common/__init__.py +14 -11
  40. siliconcompiler/tools/builtin/concatenate.py +2 -2
  41. siliconcompiler/tools/builtin/verify.py +1 -2
  42. siliconcompiler/tools/openroad/scripts/common/procs.tcl +27 -25
  43. siliconcompiler/tools/slang/__init__.py +3 -2
  44. siliconcompiler/tools/vpr/route.py +69 -0
  45. siliconcompiler/tools/yosys/sc_synth_asic.tcl +0 -4
  46. siliconcompiler/toolscripts/_tools.json +13 -8
  47. siliconcompiler/toolscripts/ubuntu22/install-klayout.sh +4 -0
  48. siliconcompiler/toolscripts/ubuntu24/install-klayout.sh +4 -0
  49. siliconcompiler/utils/__init__.py +2 -23
  50. siliconcompiler/utils/flowgraph.py +5 -5
  51. siliconcompiler/utils/logging.py +2 -1
  52. {siliconcompiler-0.33.1.dist-info → siliconcompiler-0.34.0.dist-info}/METADATA +8 -6
  53. {siliconcompiler-0.33.1.dist-info → siliconcompiler-0.34.0.dist-info}/RECORD +57 -52
  54. {siliconcompiler-0.33.1.dist-info → siliconcompiler-0.34.0.dist-info}/WHEEL +1 -1
  55. siliconcompiler/scheduler/docker_runner.py +0 -254
  56. siliconcompiler/schema/journalingschema.py +0 -238
  57. {siliconcompiler-0.33.1.dist-info → siliconcompiler-0.34.0.dist-info}/entry_points.txt +0 -0
  58. {siliconcompiler-0.33.1.dist-info → siliconcompiler-0.34.0.dist-info}/licenses/LICENSE +0 -0
  59. {siliconcompiler-0.33.1.dist-info → siliconcompiler-0.34.0.dist-info}/top_level.txt +0 -0
siliconcompiler/metric.py CHANGED
@@ -44,6 +44,25 @@ class MetricSchema(BaseSchema):
44
44
 
45
45
  return self.set(metric, value, step=step, index=str(index))
46
46
 
47
+ def record_tasktime(self, step, index, record):
48
+ """
49
+ Record the task time for this node
50
+
51
+ Args:
52
+ step (str): step to record
53
+ index (str/int): index to record
54
+ record (:class:`RecordSchema`): record to lookup data in
55
+ """
56
+ start_time, end_time = [
57
+ record.get_recorded_time(step, index, RecordTime.START),
58
+ record.get_recorded_time(step, index, RecordTime.END)
59
+ ]
60
+
61
+ if start_time is None or end_time is None:
62
+ return False
63
+
64
+ return self.record(step, index, "tasktime", end_time-start_time, unit="s")
65
+
47
66
  def record_totaltime(self, step, index, flow, record):
48
67
  """
49
68
  Record the total time for this node
@@ -1,279 +1,439 @@
1
- import os
2
- from urllib.parse import urlparse
1
+ import contextlib
2
+ import functools
3
3
  import importlib
4
- import re
5
- from siliconcompiler import SiliconCompilerError
6
- from siliconcompiler.utils import default_cache_dir
7
4
  import json
8
- from importlib.metadata import distributions, distribution
9
- import functools
5
+ import logging
6
+ import os
7
+ import re
10
8
  import time
11
- from pathlib import Path
12
-
13
- from siliconcompiler.utils import get_plugins, get_env_vars
14
- from siliconcompiler.schema.parametervalue import PathNodeValue
15
-
16
-
17
- def get_cache_path(chip):
18
- cache_path = chip.get('option', 'cachedir')
19
- if cache_path:
20
- cache_path = chip.find_files('option', 'cachedir', missing_ok=True)
21
- if not cache_path:
22
- cache_path = os.path.join(chip.cwd, chip.get('option', 'cachedir'))
23
- if not cache_path:
24
- cache_path = default_cache_dir()
25
-
26
- return cache_path
27
-
9
+ import threading
28
10
 
29
- def get_download_cache_path(chip, package, ref):
30
- cache_path = get_cache_path(chip)
31
- if not os.path.exists(cache_path):
32
- os.makedirs(cache_path, exist_ok=True)
11
+ import os.path
33
12
 
34
- if ref is None:
35
- raise SiliconCompilerError(f'Reference is required for cached data: {package}', chip=chip)
36
-
37
- return \
38
- os.path.join(cache_path, f'{package}-{ref}'), \
39
- os.path.join(cache_path, f'{package}-{ref}.lock')
13
+ from fasteners import InterProcessLock
14
+ from importlib.metadata import distributions, distribution
15
+ from pathlib import Path
16
+ from urllib import parse as url_parse
40
17
 
18
+ from siliconcompiler.utils import get_plugins
41
19
 
42
- def _file_path_resolver(chip, package, path, ref, url, fetch):
43
- return os.path.abspath(path.replace('file://', ''))
44
20
 
21
+ def path(chip, package):
22
+ import warnings
23
+ warnings.warn("The 'path' method has been deprecated",
24
+ DeprecationWarning)
25
+ return chip.get("package", field="schema").get_resolver(package).get_path()
45
26
 
46
- def _python_path_resolver(chip, package, path, ref, url, fetch):
47
- return path_from_python(chip, url.netloc)
48
27
 
28
+ def register_python_data_source(chip,
29
+ package_name,
30
+ python_module,
31
+ alternative_path,
32
+ alternative_ref=None,
33
+ python_module_path_append=None):
34
+ '''
35
+ Helper function to register a python module as data source with an alternative in case
36
+ the module is not installed in an editable state
37
+ '''
38
+ import warnings
39
+ warnings.warn("The 'register_python_data_source' method was renamed "
40
+ "PythonPathResolver.register_source",
41
+ DeprecationWarning)
42
+
43
+ PythonPathResolver.register_source(
44
+ chip, package_name, python_module, alternative_path,
45
+ alternative_ref=alternative_ref,
46
+ python_module_path_append=python_module_path_append)
47
+
48
+
49
+ class Resolver:
50
+ _RESOLVERS_LOCK = threading.Lock()
51
+ _RESOLVERS = {}
52
+
53
+ def __init__(self, name, root, source, reference=None):
54
+ self.__name = name
55
+ self.__root = root
56
+ self.__source = source
57
+ self.__reference = reference
58
+ self.__changed = False
59
+ self.__cache = {}
60
+
61
+ if self.__root and hasattr(self.__root, "logger"):
62
+ self.__logger = self.__root.logger.getChild(f"resolver-{self.name}")
63
+ else:
64
+ self.__logger = logging.getLogger(f"resolver-{self.name}")
49
65
 
50
- def _key_path_resolver(chip, package, path, ref, url, fetch):
51
- key = url.netloc.split(',')
52
- if chip.get(*key, field='pernode').is_never():
53
- paths = chip.find_files(*key)
54
- else:
55
- paths = chip.find_files(*key, step=chip.get('arg', 'step'), index=chip.get('arg', 'index'))
66
+ @staticmethod
67
+ def populate_resolvers():
68
+ with Resolver._RESOLVERS_LOCK:
69
+ Resolver._RESOLVERS.clear()
56
70
 
57
- if isinstance(paths, list):
58
- return paths[0]
59
- return paths
71
+ Resolver._RESOLVERS.update({
72
+ "": FileResolver,
73
+ "file": FileResolver,
74
+ "key": KeyPathResolver,
75
+ "python": PythonPathResolver
76
+ })
60
77
 
78
+ for resolver in get_plugins("path_resolver"):
79
+ Resolver._RESOLVERS.update(resolver())
61
80
 
62
- def _get_path_resolver(path):
63
- url = urlparse(path)
81
+ @staticmethod
82
+ def find_resolver(source):
83
+ if os.path.isabs(source):
84
+ return FileResolver
64
85
 
65
- for resolver in get_plugins("path_resolver"):
66
- func = resolver(url)
67
- if func:
68
- return func, url
86
+ if not Resolver._RESOLVERS:
87
+ Resolver.populate_resolvers()
69
88
 
70
- if url.scheme == "key":
71
- return _key_path_resolver, url
89
+ url = url_parse.urlparse(source)
90
+ with Resolver._RESOLVERS_LOCK:
91
+ if url.scheme in Resolver._RESOLVERS:
92
+ return Resolver._RESOLVERS[url.scheme]
72
93
 
73
- if url.scheme == "file":
74
- return _file_path_resolver, url
94
+ raise ValueError(f"{source} is not supported")
75
95
 
76
- if url.scheme == "python":
77
- return _python_path_resolver, url
96
+ @property
97
+ def name(self) -> str:
98
+ return self.__name
78
99
 
79
- raise ValueError(f"{path} is not supported")
100
+ @property
101
+ def root(self):
102
+ return self.__root
80
103
 
104
+ @property
105
+ def logger(self) -> logging.Logger:
106
+ return self.__logger
81
107
 
82
- def _path(chip, package, fetch):
83
- # Initially try retrieving data source from schema
84
- data = {}
85
- data['path'] = chip.get('package', 'source', package, 'path')
86
- data['ref'] = chip.get('package', 'source', package, 'ref')
87
- if not data['path']:
88
- if package.startswith("key://"):
89
- data['path'] = package
90
- else:
91
- raise SiliconCompilerError(
92
- f'Could not find package source for {package} in schema. '
93
- 'You can use register_source() to add it.', chip=chip)
108
+ @property
109
+ def source(self) -> str:
110
+ return self.__source
94
111
 
95
- env_vars = get_env_vars(chip, None, None)
96
- data['path'] = PathNodeValue.resolve_env_vars(data['path'], envvars=env_vars)
112
+ @property
113
+ def reference(self) -> str:
114
+ return self.__reference
97
115
 
98
- if os.path.exists(data['path']):
99
- # Path is already a path
100
- return os.path.abspath(data['path'])
116
+ @property
117
+ def urlparse(self) -> url_parse.ParseResult:
118
+ return url_parse.urlparse(self.__resolve_env(self.source))
101
119
 
102
- path_resolver, url = _get_path_resolver(data['path'])
120
+ @property
121
+ def urlscheme(self) -> str:
122
+ return self.urlparse.scheme
103
123
 
104
- return path_resolver(chip, package, data['path'], data['ref'], url, fetch)
124
+ @property
125
+ def urlpath(self) -> str:
126
+ return self.urlparse.netloc
105
127
 
128
+ @property
129
+ def changed(self):
130
+ change = self.__changed
131
+ self.__changed = False
132
+ return change
106
133
 
107
- def path(chip, package, fetch=True):
108
- """
109
- Compute data source data path
110
- Additionally cache data source data if possible
111
- Parameters:
112
- package (str): Name of the data source
113
- fetch (bool): Flag to indicate that the path should be fetched
114
- Returns:
115
- path: Location of data source on the local system
116
- """
134
+ def set_changed(self):
135
+ self.__changed = True
117
136
 
118
- if package not in chip._packages:
119
- changed = False
120
- data_path = _path(chip, package, fetch)
137
+ def set_cache(self, cache):
138
+ self.__cache = cache
121
139
 
122
- if isinstance(data_path, tuple) and len(data_path) == 2:
123
- data_path, changed = data_path
140
+ def resolve(self):
141
+ raise NotImplementedError("child class must implement this")
124
142
 
125
- if package.startswith("key://"):
126
- return data_path
143
+ def get_path(self):
144
+ if self.name in self.__cache:
145
+ return self.__cache[self.name]
127
146
 
128
- if os.path.exists(data_path):
129
- if package not in chip._packages and changed:
130
- chip.logger.info(f'Saved {package} data to {data_path}')
131
- else:
132
- chip.logger.info(f'Found {package} data at {data_path}')
147
+ path = self.resolve()
148
+ if not os.path.exists(path):
149
+ raise FileNotFoundError(f"Unable to locate {self.name} at {path}")
133
150
 
134
- chip._packages[package] = data_path
151
+ if self.changed and self.name not in self.__cache:
152
+ self.logger.info(f'Saved {self.name} data to {path}')
135
153
  else:
136
- raise SiliconCompilerError(f'Unable to locate {package} data in {data_path}',
137
- chip=chip)
138
-
139
- return chip._packages[package]
140
-
154
+ self.logger.info(f'Found {self.name} data at {path}')
155
+ self.__cache[self.name] = path
156
+ return self.__cache[self.name]
157
+
158
+ def __resolve_env(self, path):
159
+ env_save = os.environ.copy()
160
+
161
+ if self.root:
162
+ schema_env = {}
163
+ if self.root.valid("option", "env"):
164
+ for env in self.root.getkeys('option', 'env'):
165
+ schema_env[env] = self.root.get('option', 'env', env)
166
+ os.environ.update(schema_env)
167
+
168
+ path = os.path.expandvars(path)
169
+ path = os.path.expanduser(path)
170
+ os.environ.clear()
171
+ os.environ.update(env_save)
172
+ return path
173
+
174
+
175
+ class RemoteResolver(Resolver):
176
+ _CACHE_LOCKS = {}
177
+ _CACHE_LOCK = threading.Lock()
178
+
179
+ def __init__(self, name, root, source, reference=None):
180
+ if reference is None:
181
+ raise ValueError(f'Reference is required for cached data: {name}')
182
+
183
+ super().__init__(name, root, source, reference)
184
+
185
+ # Wait a maximum of 10 minutes for other processes to finish
186
+ self.__max_lock_wait = 60 * 10
187
+
188
+ @property
189
+ def timeout(self):
190
+ return self.__max_lock_wait
191
+
192
+ def set_timeout(self, value):
193
+ self.__max_lock_wait = value
194
+
195
+ @staticmethod
196
+ def determine_cache_dir(root) -> Path:
197
+ default_path = os.path.join(Path.home(), '.sc', 'cache')
198
+ if not root:
199
+ return Path(default_path)
200
+
201
+ if root.valid('option', 'cachedir'):
202
+ path = root.get('option', 'cachedir')
203
+ if path:
204
+ path = root.find_files('option', 'cachedir', missing_ok=True)
205
+ if not path:
206
+ path = os.path.join(getattr(root, "cwd", os.getcwd()),
207
+ root.get('option', 'cachedir'))
208
+ if not path:
209
+ path = default_path
210
+
211
+ return Path(path)
212
+
213
+ @property
214
+ def cache_dir(self) -> Path:
215
+ return RemoteResolver.determine_cache_dir(self.root)
216
+
217
+ @property
218
+ def cache_name(self) -> str:
219
+ return f"{self.name}-{self.reference}"
220
+
221
+ @property
222
+ def cache_path(self) -> Path:
223
+ cache_dir = self.cache_dir
224
+ if not os.path.exists(cache_dir):
225
+ os.makedirs(cache_dir, exist_ok=True)
226
+
227
+ return self.cache_dir / self.cache_name
228
+
229
+ @property
230
+ def lock_file(self) -> Path:
231
+ cache_dir = self.cache_dir
232
+ if not os.path.exists(cache_dir):
233
+ os.makedirs(cache_dir, exist_ok=True)
234
+
235
+ return self.cache_dir / f"{self.cache_name}.lock"
236
+
237
+ @property
238
+ def sc_lock_file(self) -> Path:
239
+ cache_dir = self.cache_dir
240
+ if not os.path.exists(cache_dir):
241
+ os.makedirs(cache_dir, exist_ok=True)
242
+
243
+ return self.cache_dir / f"{self.cache_name}.sc_lock"
244
+
245
+ def thread_lock(self):
246
+ with RemoteResolver._CACHE_LOCK:
247
+ if self.name not in RemoteResolver._CACHE_LOCKS:
248
+ RemoteResolver._CACHE_LOCKS[self.name] = threading.Lock()
249
+ return RemoteResolver._CACHE_LOCKS[self.name]
250
+
251
+ @contextlib.contextmanager
252
+ def lock(self):
253
+ lock = self.thread_lock()
254
+ lock_acquired = False
255
+ try:
256
+ if lock.acquire_lock(timeout=self.timeout):
257
+ data_path_lock = InterProcessLock(self.lock_file)
258
+ sc_data_path_lock = None
259
+ try:
260
+ lock_acquired = data_path_lock.acquire(timeout=self.timeout)
261
+ except (OSError, RuntimeError):
262
+ if not lock_acquired:
263
+ sc_data_path_lock = Path(self.sc_lock_file)
264
+ max_seconds = self.timeout
265
+ while sc_data_path_lock.exists():
266
+ if max_seconds == 0:
267
+ raise RuntimeError(f'Failed to access {self.cache_path}. '
268
+ f'Lock {sc_data_path_lock} still exists.')
269
+ time.sleep(1)
270
+ max_seconds -= 1
271
+ sc_data_path_lock.touch()
272
+ lock_acquired = True
273
+ if lock_acquired:
274
+ yield
275
+ finally:
276
+ if lock.locked():
277
+ lock.release()
278
+ if lock_acquired:
279
+ if data_path_lock.acquired:
280
+ data_path_lock.release()
281
+ if sc_data_path_lock:
282
+ sc_data_path_lock.unlink(missing_ok=True)
283
+
284
+ if not lock_acquired:
285
+ raise RuntimeError(f'Failed to access {self.cache_path}. '
286
+ f'{self.lock_file} is still locked, if this is a mistake, '
287
+ 'please delete it.')
141
288
 
142
- def __get_filebased_lock(data_lock):
143
- base, _ = os.path.splitext(os.fsdecode(data_lock.path))
144
- return Path(f'{base}.sc_lock')
289
+ def resolve_remote(self):
290
+ raise NotImplementedError("child class must implement this")
145
291
 
292
+ def check_cache(self):
293
+ raise NotImplementedError("child class must implement this")
146
294
 
147
- def aquire_data_lock(data_path, data_lock):
148
- # Wait a maximum of 10 minutes for other processes to finish
149
- max_seconds = 10 * 60
150
- try:
151
- if data_lock.acquire(timeout=max_seconds):
152
- return
153
- except RuntimeError:
154
- # Fall back to file based locking method
155
- lock_file = __get_filebased_lock(data_lock)
156
- while (lock_file.exists()):
157
- if max_seconds == 0:
158
- raise SiliconCompilerError(f'Failed to access {data_path}.'
159
- f'Lock {lock_file} still exists.')
160
- time.sleep(1)
161
- max_seconds -= 1
295
+ def resolve(self) -> Path:
296
+ cache_dir = self.cache_dir
297
+ if not os.path.exists(cache_dir):
298
+ try:
299
+ os.makedirs(cache_dir, exist_ok=True)
300
+ except OSError:
301
+ return self.cache_path
162
302
 
163
- lock_file.touch()
303
+ if not os.access(self.cache_dir, os.W_OK):
304
+ return self.cache_path
164
305
 
165
- return
306
+ with self.lock():
307
+ if self.check_cache():
308
+ return self.cache_path
166
309
 
167
- raise SiliconCompilerError(f'Failed to access {data_path}. '
168
- f'{data_lock.path} is still locked, if this is a mistake, '
169
- 'please delete it.')
310
+ self.resolve_remote()
311
+ self.set_changed()
312
+ return self.cache_path
170
313
 
171
314
 
172
- def release_data_lock(data_lock):
173
- # Check if file based locking method was used
174
- lock_file = __get_filebased_lock(data_lock)
175
- if lock_file.exists():
176
- lock_file.unlink(missing_ok=True)
177
- return
315
+ ###############
316
+ class FileResolver(Resolver):
317
+ def __init__(self, name, root, source, reference=None):
318
+ if source.startswith("file://"):
319
+ source = source[7:]
320
+ if not os.path.isabs(source):
321
+ source = os.path.join(getattr(root, "cwd", os.getcwd()), source)
178
322
 
179
- data_lock.release()
323
+ super().__init__(name, root, f"file://{source}", None)
180
324
 
325
+ @property
326
+ def urlpath(self):
327
+ parse = self.urlparse
328
+ if parse.netloc:
329
+ return parse.netloc
330
+ else:
331
+ return parse.path
181
332
 
182
- def path_from_python(chip, python_package, append_path=None):
183
- try:
184
- module = importlib.import_module(python_package)
185
- except: # noqa E722
186
- raise SiliconCompilerError(f'Failed to import {python_package}.', chip=chip)
333
+ def resolve(self):
334
+ return os.path.abspath(self.urlpath)
187
335
 
188
- python_path = os.path.dirname(module.__file__)
189
- if append_path:
190
- if isinstance(append_path, str):
191
- append_path = [append_path]
192
- python_path = os.path.join(python_path, *append_path)
193
336
 
194
- python_path = os.path.abspath(python_path)
337
+ class PythonPathResolver(Resolver):
338
+ def __init__(self, name, root, source, reference=None):
339
+ super().__init__(name, root, source, None)
195
340
 
196
- return python_path
341
+ @staticmethod
342
+ @functools.lru_cache(maxsize=1)
343
+ def get_python_module_mapping():
344
+ mapping = {}
197
345
 
346
+ for dist in distributions():
347
+ dist_name = None
348
+ if hasattr(dist, 'name'):
349
+ dist_name = dist.name
350
+ else:
351
+ metadata = dist.read_text('METADATA')
352
+ if metadata:
353
+ find_name = re.compile(r'Name: (.*)')
354
+ for data in metadata.splitlines():
355
+ group = find_name.findall(data)
356
+ if group:
357
+ dist_name = group[0]
358
+ break
359
+
360
+ if not dist_name:
361
+ continue
362
+
363
+ provides = dist.read_text('top_level.txt')
364
+ if provides:
365
+ for module in dist.read_text('top_level.txt').split():
366
+ mapping.setdefault(module, []).append(dist_name)
367
+
368
+ return mapping
369
+
370
+ @staticmethod
371
+ def is_python_module_editable(module_name):
372
+ dist_map = PythonPathResolver.get_python_module_mapping()
373
+ dist = dist_map[module_name][0]
374
+
375
+ is_editable = False
376
+ for f in distribution(dist).files:
377
+ if f.name == 'direct_url.json':
378
+ info = None
379
+ with open(f.locate(), 'r') as f:
380
+ info = json.load(f)
381
+
382
+ if "dir_info" in info:
383
+ is_editable = info["dir_info"].get("editable", False)
384
+
385
+ return is_editable
386
+
387
+ @staticmethod
388
+ def register_source(root,
389
+ package_name,
390
+ python_module,
391
+ alternative_path,
392
+ alternative_ref=None,
393
+ python_module_path_append=None):
394
+ '''
395
+ Helper function to register a python module as data source with an alternative in case
396
+ the module is not installed in an editable state
397
+ '''
398
+ # check if installed in an editable state
399
+ if PythonPathResolver.is_python_module_editable(python_module):
400
+ if python_module_path_append:
401
+ path = PythonPathResolver(
402
+ python_module, root, f"python://{python_module}").resolve()
403
+ path = os.path.abspath(os.path.join(path, python_module_path_append))
404
+ else:
405
+ path = f"python://{python_module}"
406
+ ref = None
407
+ else:
408
+ path = alternative_path
409
+ ref = alternative_ref
198
410
 
199
- def is_python_module_editable(module_name):
200
- dist_map = __get_python_module_mapping()
201
- dist = dist_map[module_name][0]
411
+ root.register_source(name=package_name,
412
+ path=path,
413
+ ref=ref)
202
414
 
203
- is_editable = False
204
- for f in distribution(dist).files:
205
- if f.name == 'direct_url.json':
206
- info = None
207
- with open(f.locate(), 'r') as f:
208
- info = json.load(f)
415
+ def resolve(self):
416
+ module = importlib.import_module(self.urlpath)
417
+ python_path = os.path.dirname(module.__file__)
418
+ return os.path.abspath(python_path)
209
419
 
210
- if "dir_info" in info:
211
- is_editable = info["dir_info"].get("editable", False)
212
420
 
213
- return is_editable
421
+ class KeyPathResolver(Resolver):
422
+ def __init__(self, name, root, source, reference=None):
423
+ super().__init__(name, root, source, None)
214
424
 
425
+ def resolve(self):
426
+ if not self.root:
427
+ raise RuntimeError(f"Root schema has not be defined for {self.name}")
215
428
 
216
- def register_python_data_source(chip,
217
- package_name,
218
- python_module,
219
- alternative_path,
220
- alternative_ref=None,
221
- python_module_path_append=None):
222
- '''
223
- Helper function to register a python module as data source with an alternative in case
224
- the module is not installed in an editable state
225
- '''
226
- # check if installed in an editable state
227
- if is_python_module_editable(python_module):
228
- if python_module_path_append:
229
- path = path_from_python(chip, python_module, append_path=python_module_path_append)
429
+ key = self.urlpath.split(",")
430
+ if self.root.get(*key, field='pernode').is_never():
431
+ paths = self.root.find_files(*key)
230
432
  else:
231
- path = f"python://{python_module}"
232
- ref = None
233
- else:
234
- path = alternative_path
235
- ref = alternative_ref
236
-
237
- chip.register_source(name=package_name,
238
- path=path,
239
- ref=ref)
433
+ paths = self.root.find_files(*key,
434
+ step=self.root.get('arg', 'step'),
435
+ index=self.root.get('arg', 'index'))
240
436
 
241
-
242
- @functools.lru_cache(maxsize=1)
243
- def __get_python_module_mapping():
244
- mapping = {}
245
-
246
- for dist in distributions():
247
- dist_name = None
248
- if hasattr(dist, 'name'):
249
- dist_name = dist.name
250
- else:
251
- metadata = dist.read_text('METADATA')
252
- if metadata:
253
- find_name = re.compile(r'Name: (.*)')
254
- for data in metadata.splitlines():
255
- group = find_name.findall(data)
256
- if group:
257
- dist_name = group[0]
258
- break
259
-
260
- if not dist_name:
261
- continue
262
-
263
- provides = dist.read_text('top_level.txt')
264
- if provides:
265
- for module in dist.read_text('top_level.txt').split():
266
- mapping.setdefault(module, []).append(dist_name)
267
-
268
- return mapping
269
-
270
-
271
- def register_private_github_data_source(chip,
272
- package_name,
273
- repository,
274
- release,
275
- artifact):
276
- chip.register_source(
277
- package_name,
278
- path=f"github+private://{repository}/{release}/{artifact}",
279
- ref=release)
437
+ if isinstance(paths, list):
438
+ return paths[0]
439
+ return paths