siliconcompiler 0.34.2__py3-none-any.whl → 0.34.3__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 (121) hide show
  1. siliconcompiler/__init__.py +12 -5
  2. siliconcompiler/__main__.py +1 -7
  3. siliconcompiler/_metadata.py +1 -1
  4. siliconcompiler/apps/_common.py +104 -23
  5. siliconcompiler/apps/sc.py +4 -8
  6. siliconcompiler/apps/sc_dashboard.py +6 -4
  7. siliconcompiler/apps/sc_install.py +10 -6
  8. siliconcompiler/apps/sc_issue.py +7 -5
  9. siliconcompiler/apps/sc_remote.py +1 -1
  10. siliconcompiler/apps/sc_server.py +9 -14
  11. siliconcompiler/apps/sc_show.py +6 -5
  12. siliconcompiler/apps/smake.py +130 -94
  13. siliconcompiler/apps/utils/replay.py +4 -7
  14. siliconcompiler/apps/utils/summarize.py +3 -5
  15. siliconcompiler/asic.py +420 -0
  16. siliconcompiler/checklist.py +25 -2
  17. siliconcompiler/cmdlineschema.py +534 -0
  18. siliconcompiler/constraints/asic_component.py +2 -2
  19. siliconcompiler/constraints/asic_pins.py +2 -2
  20. siliconcompiler/constraints/asic_timing.py +3 -3
  21. siliconcompiler/core.py +7 -32
  22. siliconcompiler/data/templates/tcl/manifest.tcl.j2 +8 -0
  23. siliconcompiler/dependencyschema.py +89 -31
  24. siliconcompiler/design.py +176 -207
  25. siliconcompiler/filesetschema.py +250 -0
  26. siliconcompiler/flowgraph.py +274 -95
  27. siliconcompiler/fpga.py +124 -1
  28. siliconcompiler/library.py +218 -20
  29. siliconcompiler/metric.py +233 -20
  30. siliconcompiler/package/__init__.py +271 -50
  31. siliconcompiler/package/git.py +92 -16
  32. siliconcompiler/package/github.py +108 -12
  33. siliconcompiler/package/https.py +79 -16
  34. siliconcompiler/packageschema.py +88 -7
  35. siliconcompiler/pathschema.py +31 -2
  36. siliconcompiler/pdk.py +566 -1
  37. siliconcompiler/project.py +1095 -94
  38. siliconcompiler/record.py +38 -1
  39. siliconcompiler/remote/__init__.py +5 -2
  40. siliconcompiler/remote/client.py +11 -6
  41. siliconcompiler/remote/schema.py +5 -23
  42. siliconcompiler/remote/server.py +41 -54
  43. siliconcompiler/report/__init__.py +3 -3
  44. siliconcompiler/report/dashboard/__init__.py +48 -14
  45. siliconcompiler/report/dashboard/cli/__init__.py +99 -21
  46. siliconcompiler/report/dashboard/cli/board.py +364 -179
  47. siliconcompiler/report/dashboard/web/__init__.py +90 -12
  48. siliconcompiler/report/dashboard/web/components/__init__.py +219 -240
  49. siliconcompiler/report/dashboard/web/components/flowgraph.py +49 -26
  50. siliconcompiler/report/dashboard/web/components/graph.py +139 -100
  51. siliconcompiler/report/dashboard/web/layouts/__init__.py +29 -1
  52. siliconcompiler/report/dashboard/web/layouts/_common.py +38 -2
  53. siliconcompiler/report/dashboard/web/layouts/vertical_flowgraph.py +39 -26
  54. siliconcompiler/report/dashboard/web/layouts/vertical_flowgraph_node_tab.py +50 -50
  55. siliconcompiler/report/dashboard/web/layouts/vertical_flowgraph_sac_tabs.py +49 -46
  56. siliconcompiler/report/dashboard/web/state.py +141 -14
  57. siliconcompiler/report/dashboard/web/utils/__init__.py +79 -16
  58. siliconcompiler/report/dashboard/web/utils/file_utils.py +74 -11
  59. siliconcompiler/report/dashboard/web/viewer.py +25 -1
  60. siliconcompiler/report/report.py +5 -2
  61. siliconcompiler/report/summary_image.py +29 -11
  62. siliconcompiler/scheduler/__init__.py +9 -1
  63. siliconcompiler/scheduler/docker.py +79 -1
  64. siliconcompiler/scheduler/run_node.py +35 -19
  65. siliconcompiler/scheduler/scheduler.py +208 -24
  66. siliconcompiler/scheduler/schedulernode.py +372 -46
  67. siliconcompiler/scheduler/send_messages.py +77 -29
  68. siliconcompiler/scheduler/slurm.py +76 -12
  69. siliconcompiler/scheduler/taskscheduler.py +140 -20
  70. siliconcompiler/schema/__init__.py +0 -2
  71. siliconcompiler/schema/baseschema.py +194 -38
  72. siliconcompiler/schema/journal.py +7 -4
  73. siliconcompiler/schema/namedschema.py +16 -10
  74. siliconcompiler/schema/parameter.py +55 -9
  75. siliconcompiler/schema/parametervalue.py +60 -0
  76. siliconcompiler/schema/safeschema.py +25 -2
  77. siliconcompiler/schema/schema_cfg.py +5 -5
  78. siliconcompiler/schema/utils.py +2 -2
  79. siliconcompiler/schema_obj.py +20 -3
  80. siliconcompiler/tool.py +979 -302
  81. siliconcompiler/tools/bambu/__init__.py +41 -0
  82. siliconcompiler/tools/builtin/concatenate.py +2 -2
  83. siliconcompiler/tools/builtin/minimum.py +2 -1
  84. siliconcompiler/tools/builtin/mux.py +2 -1
  85. siliconcompiler/tools/builtin/nop.py +2 -1
  86. siliconcompiler/tools/builtin/verify.py +2 -1
  87. siliconcompiler/tools/klayout/__init__.py +95 -0
  88. siliconcompiler/tools/openroad/__init__.py +289 -0
  89. siliconcompiler/tools/openroad/scripts/apr/preamble.tcl +3 -0
  90. siliconcompiler/tools/openroad/scripts/apr/sc_detailed_route.tcl +7 -2
  91. siliconcompiler/tools/openroad/scripts/apr/sc_global_route.tcl +8 -4
  92. siliconcompiler/tools/openroad/scripts/apr/sc_init_floorplan.tcl +9 -5
  93. siliconcompiler/tools/openroad/scripts/common/write_images.tcl +5 -1
  94. siliconcompiler/tools/slang/__init__.py +1 -1
  95. siliconcompiler/tools/slang/elaborate.py +2 -1
  96. siliconcompiler/tools/vivado/scripts/sc_run.tcl +1 -1
  97. siliconcompiler/tools/vivado/scripts/sc_syn_fpga.tcl +8 -1
  98. siliconcompiler/tools/vivado/syn_fpga.py +6 -0
  99. siliconcompiler/tools/vivado/vivado.py +35 -2
  100. siliconcompiler/tools/vpr/__init__.py +150 -0
  101. siliconcompiler/tools/yosys/__init__.py +369 -1
  102. siliconcompiler/tools/yosys/scripts/procs.tcl +0 -1
  103. siliconcompiler/toolscripts/_tools.json +5 -10
  104. siliconcompiler/utils/__init__.py +66 -0
  105. siliconcompiler/utils/flowgraph.py +2 -2
  106. siliconcompiler/utils/issue.py +2 -1
  107. siliconcompiler/utils/logging.py +14 -0
  108. siliconcompiler/utils/multiprocessing.py +256 -0
  109. siliconcompiler/utils/showtools.py +10 -0
  110. {siliconcompiler-0.34.2.dist-info → siliconcompiler-0.34.3.dist-info}/METADATA +5 -5
  111. {siliconcompiler-0.34.2.dist-info → siliconcompiler-0.34.3.dist-info}/RECORD +115 -118
  112. {siliconcompiler-0.34.2.dist-info → siliconcompiler-0.34.3.dist-info}/entry_points.txt +3 -0
  113. siliconcompiler/schema/cmdlineschema.py +0 -250
  114. siliconcompiler/toolscripts/rhel8/install-slang.sh +0 -40
  115. siliconcompiler/toolscripts/rhel9/install-slang.sh +0 -40
  116. siliconcompiler/toolscripts/ubuntu20/install-slang.sh +0 -47
  117. siliconcompiler/toolscripts/ubuntu22/install-slang.sh +0 -37
  118. siliconcompiler/toolscripts/ubuntu24/install-slang.sh +0 -37
  119. {siliconcompiler-0.34.2.dist-info → siliconcompiler-0.34.3.dist-info}/WHEEL +0 -0
  120. {siliconcompiler-0.34.2.dist-info → siliconcompiler-0.34.3.dist-info}/licenses/LICENSE +0 -0
  121. {siliconcompiler-0.34.2.dist-info → siliconcompiler-0.34.3.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,12 @@
1
+ """
2
+ This module defines the core path resolution system for SiliconCompiler packages.
3
+
4
+ It provides a flexible mechanism to locate data sources, whether they are on
5
+ the local filesystem, remote servers, or part of a Python package. The system
6
+ is designed to be extensible, allowing new resolver types to be added as plugins.
7
+ It also includes robust caching and locking mechanisms for handling remote data
8
+ efficiently and safely in multi-process and multi-threaded environments.
9
+ """
1
10
  import contextlib
2
11
  import functools
3
12
  import hashlib
@@ -22,6 +31,7 @@ from siliconcompiler.utils import get_plugins
22
31
 
23
32
 
24
33
  def path(chip, package):
34
+ """DEPRECATED: Use chip.get_resolver(package).get_path() instead."""
25
35
  import warnings
26
36
  warnings.warn("The 'path' method has been deprecated",
27
37
  DeprecationWarning)
@@ -34,10 +44,7 @@ def register_python_data_source(chip,
34
44
  alternative_path,
35
45
  alternative_ref=None,
36
46
  python_module_path_append=None):
37
- '''
38
- Helper function to register a python module as data source with an alternative in case
39
- the module is not installed in an editable state
40
- '''
47
+ """DEPRECATED: Use PythonPathResolver.register_source() instead."""
41
48
  import warnings
42
49
  warnings.warn("The 'register_python_data_source' method was renamed "
43
50
  "PythonPathResolver.register_source",
@@ -50,6 +57,20 @@ def register_python_data_source(chip,
50
57
 
51
58
 
52
59
  class Resolver:
60
+ """
61
+ Abstract base class for all data source resolvers.
62
+
63
+ This class defines the common interface for locating and accessing data
64
+ from various sources. It includes a caching mechanism to avoid redundant
65
+ resolutions within a single run.
66
+
67
+ Attributes:
68
+ name (str): The name of the data package being resolved.
69
+ root (object): The root object (typically a Chip) providing context,
70
+ such as environment variables and the working directory.
71
+ source (str): The URI or path specifying the data source.
72
+ reference (str): A version, commit hash, or tag for remote sources.
73
+ """
53
74
  _RESOLVERS_LOCK = threading.Lock()
54
75
  _RESOLVERS = {}
55
76
 
@@ -57,6 +78,9 @@ class Resolver:
57
78
  __CACHE = {}
58
79
 
59
80
  def __init__(self, name, root, source, reference=None):
81
+ """
82
+ Initializes the Resolver.
83
+ """
60
84
  self.__name = name
61
85
  self.__root = root
62
86
  self.__source = source
@@ -71,6 +95,13 @@ class Resolver:
71
95
 
72
96
  @staticmethod
73
97
  def populate_resolvers():
98
+ """
99
+ Scans for and registers all available resolver plugins.
100
+
101
+ This method populates the internal `_RESOLVERS` dictionary with both
102
+ built-in resolvers (file, key, python) and any resolvers provided
103
+ by external plugins.
104
+ """
74
105
  with Resolver._RESOLVERS_LOCK:
75
106
  Resolver._RESOLVERS.clear()
76
107
 
@@ -86,6 +117,18 @@ class Resolver:
86
117
 
87
118
  @staticmethod
88
119
  def find_resolver(source):
120
+ """
121
+ Finds the appropriate resolver class for a given source URI.
122
+
123
+ Args:
124
+ source (str): The source URI (e.g., 'file:///path/to/file', 'git://...').
125
+
126
+ Returns:
127
+ Resolver: The resolver class capable of handling the source.
128
+
129
+ Raises:
130
+ ValueError: If no suitable resolver is found for the URI scheme.
131
+ """
89
132
  if os.path.isabs(source):
90
133
  return FileResolver
91
134
 
@@ -97,67 +140,89 @@ class Resolver:
97
140
  if url.scheme in Resolver._RESOLVERS:
98
141
  return Resolver._RESOLVERS[url.scheme]
99
142
 
100
- raise ValueError(f"{source} is not supported")
143
+ raise ValueError(f"Source URI '{source}' is not supported")
101
144
 
102
145
  @property
103
146
  def name(self) -> str:
147
+ """The name of the data package being resolved."""
104
148
  return self.__name
105
149
 
106
150
  @property
107
151
  def root(self):
152
+ """The root object (e.g., Chip) providing context."""
108
153
  return self.__root
109
154
 
110
155
  @property
111
156
  def logger(self) -> logging.Logger:
157
+ """The logger instance for this resolver."""
112
158
  return self.__logger
113
159
 
114
160
  @property
115
161
  def source(self) -> str:
162
+ """The URI or path specifying the data source."""
116
163
  return self.__source
117
164
 
118
165
  @property
119
166
  def reference(self) -> str:
167
+ """A version, commit hash, or tag for the source."""
120
168
  return self.__reference
121
169
 
122
170
  @property
123
171
  def urlparse(self) -> url_parse.ParseResult:
172
+ """The parsed URL of the source after environment variable expansion."""
124
173
  return url_parse.urlparse(self.__resolve_env(self.source))
125
174
 
126
175
  @property
127
176
  def urlscheme(self) -> str:
177
+ """The scheme of the source URL (e.g., 'file', 'git')."""
128
178
  return self.urlparse.scheme
129
179
 
130
180
  @property
131
181
  def urlpath(self) -> str:
182
+ """The path component of the source URL."""
132
183
  return self.urlparse.netloc
133
184
 
134
185
  @property
135
186
  def changed(self) -> bool:
187
+ """
188
+ Indicates if the resolved data has changed (e.g., was newly fetched).
189
+
190
+ This flag is reset to False after being read.
191
+ """
136
192
  change = self.__changed
137
193
  self.__changed = False
138
194
  return change
139
195
 
140
196
  @property
141
197
  def cache_id(self) -> str:
198
+ """A unique ID for this resolver instance, used for caching."""
142
199
  if self.__cacheid is None:
143
- hash = hashlib.sha1()
144
- hash.update(self.__source.encode())
200
+ hash_obj = hashlib.sha1()
201
+ hash_obj.update(self.__source.encode())
145
202
  if self.__reference:
146
- hash.update(self.__reference.encode())
203
+ hash_obj.update(self.__reference.encode())
147
204
  else:
148
- hash.update("".encode())
205
+ hash_obj.update("".encode())
149
206
 
150
- self.__cacheid = hash.hexdigest()
207
+ self.__cacheid = hash_obj.hexdigest()
151
208
  return self.__cacheid
152
209
 
153
210
  def set_changed(self):
211
+ """Marks the resolved data as having been changed."""
154
212
  self.__changed = True
155
213
 
156
214
  def resolve(self):
215
+ """
216
+ Abstract method to perform the actual data resolution.
217
+
218
+ Subclasses must implement this method to locate or fetch the data
219
+ and return its local path.
220
+ """
157
221
  raise NotImplementedError("child class must implement this")
158
222
 
159
223
  @staticmethod
160
224
  def __get_root_id(root):
225
+ """Generates or retrieves a unique ID for a root object."""
161
226
  STORAGE = "__Resolver_cache_id"
162
227
  if not getattr(root, STORAGE, None):
163
228
  setattr(root, STORAGE, uuid.uuid4().hex)
@@ -165,6 +230,17 @@ class Resolver:
165
230
 
166
231
  @staticmethod
167
232
  def get_cache(root, name: str = None):
233
+ """
234
+ Gets a cached path for a given root object and resolver name.
235
+
236
+ Args:
237
+ root: The root object (e.g., Chip).
238
+ name (str, optional): The name of the resolver cache to retrieve.
239
+ If None, returns a copy of the entire cache for the root.
240
+
241
+ Returns:
242
+ str or dict or None: The cached path, a copy of the cache, or None.
243
+ """
168
244
  with Resolver.__CACHE_LOCK:
169
245
  root_id = Resolver.__get_root_id(root)
170
246
  if root_id not in Resolver.__CACHE:
@@ -177,6 +253,14 @@ class Resolver:
177
253
 
178
254
  @staticmethod
179
255
  def set_cache(root, name: str, path: str):
256
+ """
257
+ Sets a cached path for a given root object and resolver name.
258
+
259
+ Args:
260
+ root: The root object (e.g., Chip).
261
+ name (str): The name of the resolver cache to set.
262
+ path (str): The path to cache.
263
+ """
180
264
  with Resolver.__CACHE_LOCK:
181
265
  root_id = Resolver.__get_root_id(root)
182
266
  if root_id not in Resolver.__CACHE:
@@ -185,19 +269,37 @@ class Resolver:
185
269
 
186
270
  @staticmethod
187
271
  def reset_cache(root):
272
+ """
273
+ Resets the entire cache for a given root object.
274
+
275
+ Args:
276
+ root: The root object whose cache will be cleared.
277
+ """
188
278
  with Resolver.__CACHE_LOCK:
189
279
  root_id = Resolver.__get_root_id(root)
190
280
  if root_id in Resolver.__CACHE:
191
281
  del Resolver.__CACHE[root_id]
192
282
 
193
283
  def get_path(self):
284
+ """
285
+ Resolves the data source and returns its local path.
286
+
287
+ This method first checks the in-memory cache. If not found, it calls
288
+ the `resolve()` method and caches the result.
289
+
290
+ Returns:
291
+ str: The absolute path to the resolved data on the local filesystem.
292
+
293
+ Raises:
294
+ FileNotFoundError: If the resolved path does not exist.
295
+ """
194
296
  cache_path = Resolver.get_cache(self.__root, self.cache_id)
195
297
  if cache_path:
196
298
  return cache_path
197
299
 
198
300
  path = self.resolve()
199
301
  if not os.path.exists(path):
200
- raise FileNotFoundError(f"Unable to locate {self.name} at {path}")
302
+ raise FileNotFoundError(f"Unable to locate '{self.name}' at {path}")
201
303
 
202
304
  if self.changed:
203
305
  self.logger.info(f'Saved {self.name} data to {path}')
@@ -208,6 +310,7 @@ class Resolver:
208
310
  return path
209
311
 
210
312
  def __resolve_env(self, path):
313
+ """Expands environment variables and user home directory in a path."""
211
314
  env_save = os.environ.copy()
212
315
 
213
316
  if self.root:
@@ -225,12 +328,20 @@ class Resolver:
225
328
 
226
329
 
227
330
  class RemoteResolver(Resolver):
331
+ """
332
+ An abstract base class for resolvers that fetch data from remote sources.
333
+
334
+ This class extends `Resolver` with functionality for managing a persistent
335
+ on-disk cache in `~/.sc/cache` or a user-defined location. It implements
336
+ both thread-safe and process-safe locking to prevent race conditions when
337
+ multiple SC instances try to download the same resource simultaneously.
338
+ """
228
339
  _CACHE_LOCKS = {}
229
340
  _CACHE_LOCK = threading.Lock()
230
341
 
231
342
  def __init__(self, name, root, source, reference=None):
232
343
  if reference is None:
233
- raise ValueError(f'Reference is required for cached data: {name}')
344
+ raise ValueError(f'A reference (e.g., version, commit) is required for {name}')
234
345
 
235
346
  super().__init__(name, root, source, reference)
236
347
 
@@ -239,13 +350,27 @@ class RemoteResolver(Resolver):
239
350
 
240
351
  @property
241
352
  def timeout(self):
353
+ """The maximum time in seconds to wait for a lock."""
242
354
  return self.__max_lock_wait
243
355
 
244
356
  def set_timeout(self, value):
357
+ """Sets the maximum time in seconds to wait for a lock."""
245
358
  self.__max_lock_wait = value
246
359
 
247
360
  @staticmethod
248
361
  def determine_cache_dir(root) -> Path:
362
+ """
363
+ Determines the directory for the on-disk cache.
364
+
365
+ The location is determined by ['option', 'cachedir'] if set, otherwise
366
+ it defaults to `~/.sc/cache`.
367
+
368
+ Args:
369
+ root: The root Chip object.
370
+
371
+ Returns:
372
+ Path: The path to the cache directory.
373
+ """
249
374
  default_path = os.path.join(Path.home(), '.sc', 'cache')
250
375
  if not root:
251
376
  return Path(default_path)
@@ -265,14 +390,17 @@ class RemoteResolver(Resolver):
265
390
 
266
391
  @property
267
392
  def cache_dir(self) -> Path:
393
+ """The directory for the on-disk cache."""
268
394
  return RemoteResolver.determine_cache_dir(self.root)
269
395
 
270
396
  @property
271
397
  def cache_name(self) -> str:
272
- return f"{self.name}-{self.reference}"
398
+ """A unique name for the cached data directory."""
399
+ return f"{self.name}-{self.reference[0:16]}-{self.cache_id[0:16]}"
273
400
 
274
401
  @property
275
402
  def cache_path(self) -> Path:
403
+ """The full path to the cached data directory."""
276
404
  cache_dir = self.cache_dir
277
405
  if not os.path.exists(cache_dir):
278
406
  os.makedirs(cache_dir, exist_ok=True)
@@ -281,6 +409,7 @@ class RemoteResolver(Resolver):
281
409
 
282
410
  @property
283
411
  def lock_file(self) -> Path:
412
+ """The path to the file used for inter-process locking."""
284
413
  cache_dir = self.cache_dir
285
414
  if not os.path.exists(cache_dir):
286
415
  os.makedirs(cache_dir, exist_ok=True)
@@ -289,6 +418,9 @@ class RemoteResolver(Resolver):
289
418
 
290
419
  @property
291
420
  def sc_lock_file(self) -> Path:
421
+ """
422
+ The path to a secondary lock file used as a fallback mechanism.
423
+ """
292
424
  cache_dir = self.cache_dir
293
425
  if not os.path.exists(cache_dir):
294
426
  os.makedirs(cache_dir, exist_ok=True)
@@ -296,6 +428,7 @@ class RemoteResolver(Resolver):
296
428
  return self.cache_dir / f"{self.cache_name}.sc_lock"
297
429
 
298
430
  def thread_lock(self):
431
+ """Gets a threading.Lock specific to this resolver instance."""
299
432
  with RemoteResolver._CACHE_LOCK:
300
433
  if self.name not in RemoteResolver._CACHE_LOCKS:
301
434
  RemoteResolver._CACHE_LOCKS[self.name] = threading.Lock()
@@ -303,6 +436,7 @@ class RemoteResolver(Resolver):
303
436
 
304
437
  @contextlib.contextmanager
305
438
  def __thread_lock(self):
439
+ """A context manager for acquiring the thread lock with a timeout."""
306
440
  lock = self.thread_lock()
307
441
  lock_acquired = False
308
442
  try:
@@ -326,6 +460,7 @@ class RemoteResolver(Resolver):
326
460
 
327
461
  @contextlib.contextmanager
328
462
  def __file_lock(self):
463
+ """A context manager for acquiring the inter-process file lock."""
329
464
  data_path_lock = InterProcessLock(self.lock_file)
330
465
  lock_acquired = False
331
466
  sc_data_path_lock = None
@@ -355,30 +490,54 @@ class RemoteResolver(Resolver):
355
490
 
356
491
  if not lock_acquired:
357
492
  raise RuntimeError(f'Failed to access {self.cache_path}. '
358
- f'{self.lock_file} is still locked, if this is a mistake, '
359
- 'please delete it.')
493
+ f'{self.lock_file} is still locked. If this is a mistake, '
494
+ 'please delete the lock file.')
360
495
 
361
496
  @contextlib.contextmanager
362
497
  def lock(self):
498
+ """
499
+ A context manager that acquires both the thread and file locks.
500
+
501
+ This ensures that only one thread in one process can access the cache
502
+ for a specific resource at a time.
503
+ """
363
504
  with self.__thread_lock():
364
505
  with self.__file_lock():
365
506
  yield
366
507
 
367
508
  def resolve_remote(self):
509
+ """Abstract method to fetch the remote data."""
368
510
  raise NotImplementedError("child class must implement this")
369
511
 
370
512
  def check_cache(self):
513
+ """
514
+ Abstract method to check if the on-disk cache is valid.
515
+
516
+ Returns:
517
+ bool: True if the cache is valid, False otherwise.
518
+ """
371
519
  raise NotImplementedError("child class must implement this")
372
520
 
373
521
  def resolve(self) -> Path:
522
+ """
523
+ Resolves the remote data, using the on-disk cache if possible.
524
+
525
+ This method acquires locks, checks the cache validity, and calls
526
+ `resolve_remote()` to fetch the data if the cache is missing or invalid.
527
+
528
+ Returns:
529
+ Path: The path to the locally cached data.
530
+ """
374
531
  cache_dir = self.cache_dir
375
532
  if not os.path.exists(cache_dir):
376
533
  try:
377
534
  os.makedirs(cache_dir, exist_ok=True)
378
535
  except OSError:
536
+ # Can't create directory, return path and let it fail later
379
537
  return self.cache_path
380
538
 
381
539
  if not os.access(self.cache_dir, os.W_OK):
540
+ # Can't write to directory, assume cache is valid if it exists
382
541
  return self.cache_path
383
542
 
384
543
  with self.lock():
@@ -390,8 +549,14 @@ class RemoteResolver(Resolver):
390
549
  return self.cache_path
391
550
 
392
551
 
393
- ###############
394
552
  class FileResolver(Resolver):
553
+ """
554
+ A resolver for local file system paths.
555
+
556
+ It handles both absolute paths and paths relative to the chip's CWD.
557
+ It normalizes the source string to a `file://` URI.
558
+ """
559
+
395
560
  def __init__(self, name, root, source, reference=None):
396
561
  if source.startswith("file://"):
397
562
  source = source[7:]
@@ -402,27 +567,43 @@ class FileResolver(Resolver):
402
567
 
403
568
  @property
404
569
  def urlpath(self):
570
+ """The absolute file path, stripped of the 'file://' prefix."""
405
571
  # Rebuild URL and remove scheme prefix
406
572
  return self.urlparse.geturl()[7:]
407
573
 
408
574
  def resolve(self):
575
+ """Returns the absolute path to the file."""
409
576
  return os.path.abspath(self.urlpath)
410
577
 
411
578
 
412
579
  class PythonPathResolver(Resolver):
580
+ """
581
+ A resolver for locating installed Python packages.
582
+
583
+ This resolver uses Python's import machinery to find the installation
584
+ directory of a given Python module. It also includes helper methods to
585
+ determine if a package is installed in "editable" mode.
586
+ """
587
+
413
588
  def __init__(self, name, root, source, reference=None):
414
589
  super().__init__(name, root, source, None)
415
590
 
416
591
  @staticmethod
417
592
  @functools.lru_cache(maxsize=1)
418
593
  def get_python_module_mapping():
594
+ """
595
+ Creates a mapping from importable module names to their distribution names.
596
+
597
+ This is used to find the distribution package that provides a given module.
598
+
599
+ Returns:
600
+ dict: A dictionary mapping module names to a list of distribution names.
601
+ """
419
602
  mapping = {}
420
603
 
421
604
  for dist in distributions():
422
- dist_name = None
423
- if hasattr(dist, 'name'):
424
- dist_name = dist.name
425
- else:
605
+ dist_name = getattr(dist, 'name', None)
606
+ if not dist_name:
426
607
  metadata = dist.read_text('METADATA')
427
608
  if metadata:
428
609
  find_name = re.compile(r'Name: (.*)')
@@ -444,15 +625,30 @@ class PythonPathResolver(Resolver):
444
625
 
445
626
  @staticmethod
446
627
  def is_python_module_editable(module_name):
628
+ """
629
+ Checks if a Python module is installed in "editable" mode.
630
+
631
+ Args:
632
+ module_name (str): The name of the Python module to check.
633
+
634
+ Returns:
635
+ bool: True if the module is installed in editable mode, False otherwise.
636
+ """
447
637
  dist_map = PythonPathResolver.get_python_module_mapping()
448
- dist = dist_map[module_name][0]
638
+ if module_name not in dist_map:
639
+ return False
640
+ dist_name = dist_map[module_name][0]
449
641
 
450
642
  is_editable = False
451
- for f in distribution(dist).files:
643
+ dist_obj = distribution(dist_name)
644
+ if not dist_obj or not dist_obj.files:
645
+ return False
646
+
647
+ for f in dist_obj.files:
452
648
  if f.name == 'direct_url.json':
453
649
  info = None
454
- with open(f.locate(), 'r') as f:
455
- info = json.load(f)
650
+ with open(f.locate(), 'r') as fp:
651
+ info = json.load(fp)
456
652
 
457
653
  if "dir_info" in info:
458
654
  is_editable = info["dir_info"].get("editable", False)
@@ -466,26 +662,33 @@ class PythonPathResolver(Resolver):
466
662
  alternative_path,
467
663
  alternative_ref=None,
468
664
  python_module_path_append=None):
469
- '''
470
- Helper function to register a python module as data source with an alternative in case
471
- the module is not installed in an editable state
472
- '''
473
- # check if installed in an editable state
665
+ """
666
+ Helper to conditionally register a Python module or a fallback path.
667
+
668
+ If the specified `python_module` is installed in editable mode, it's
669
+ registered as the source. Otherwise, the `alternative_path` and
670
+ `alternative_ref` are used.
671
+
672
+ Args:
673
+ root (Chip): The chip object to register the source with.
674
+ package_name (str): The name of the package to register.
675
+ python_module (str): The Python module to check for.
676
+ alternative_path (str): The fallback source path.
677
+ alternative_ref (str, optional): The fallback reference. Defaults to None.
678
+ python_module_path_append (str, optional): A subdirectory to append
679
+ to the resolved Python module path. Defaults to None.
680
+ """
474
681
  if PythonPathResolver.is_python_module_editable(python_module):
682
+ path = f"python://{python_module}"
475
683
  if python_module_path_append:
476
- path = PythonPathResolver(
477
- python_module, root, f"python://{python_module}").resolve()
478
- path = os.path.abspath(os.path.join(path, python_module_path_append))
479
- else:
480
- path = f"python://{python_module}"
684
+ py_path = PythonPathResolver(python_module, root, path).resolve()
685
+ path = os.path.abspath(os.path.join(py_path, python_module_path_append))
481
686
  ref = None
482
687
  else:
483
688
  path = alternative_path
484
689
  ref = alternative_ref
485
690
 
486
- root.register_source(name=package_name,
487
- path=path,
488
- ref=ref)
691
+ root.register_source(name=package_name, path=path, ref=ref)
489
692
 
490
693
  @staticmethod
491
694
  def set_dataroot(root,
@@ -494,40 +697,58 @@ class PythonPathResolver(Resolver):
494
697
  alternative_path,
495
698
  alternative_ref=None,
496
699
  python_module_path_append=None):
497
- '''
498
- Helper function to register a python module as data source with an alternative in case
499
- the module is not installed in an editable state
500
- '''
700
+ """
701
+ DEPRECATED: Use register_source.
702
+ Helper to conditionally set a dataroot to a Python module or a fallback path.
703
+ """
501
704
  # check if installed in an editable state
502
705
  if PythonPathResolver.is_python_module_editable(python_module):
706
+ path = f"python://{python_module}"
503
707
  if python_module_path_append:
504
- path = PythonPathResolver(
505
- python_module, root, f"python://{python_module}").resolve()
506
- path = os.path.abspath(os.path.join(path, python_module_path_append))
507
- else:
508
- path = f"python://{python_module}"
708
+ py_path = PythonPathResolver(python_module, root, path).resolve()
709
+ path = os.path.abspath(os.path.join(py_path, python_module_path_append))
509
710
  ref = None
510
711
  else:
511
712
  path = alternative_path
512
713
  ref = alternative_ref
513
714
 
514
- root.set_dataroot(name=package_name,
515
- path=path,
516
- tag=ref)
715
+ root.set_dataroot(package_name, path=path, tag=ref)
517
716
 
518
717
  def resolve(self):
718
+ """
719
+ Resolves the path to the specified Python module.
720
+
721
+ Returns:
722
+ str: The absolute path to the module's directory.
723
+ """
519
724
  module = importlib.import_module(self.urlpath)
520
725
  python_path = os.path.dirname(module.__file__)
521
726
  return os.path.abspath(python_path)
522
727
 
523
728
 
524
729
  class KeyPathResolver(Resolver):
730
+ """
731
+ A resolver for finding file paths stored within the Chip schema itself.
732
+
733
+ This resolver takes a keypath (e.g., 'tool,openroad,exe') and uses the
734
+ `find_files` method of the root Chip object to locate the corresponding file.
735
+ """
736
+
525
737
  def __init__(self, name, root, source, reference=None):
526
738
  super().__init__(name, root, source, None)
527
739
 
528
740
  def resolve(self):
741
+ """
742
+ Resolves the path by looking up the keypath in the Chip schema.
743
+
744
+ Returns:
745
+ str: The file path found in the schema.
746
+
747
+ Raises:
748
+ RuntimeError: If the resolver does not have a root Chip object defined.
749
+ """
529
750
  if not self.root:
530
- raise RuntimeError(f"Root schema has not be defined for {self.name}")
751
+ raise RuntimeError(f"A root schema has not been defined for '{self.name}'")
531
752
 
532
753
  key = self.urlpath.split(",")
533
754
  if self.root.get(*key, field='pernode').is_never():