siliconcompiler 0.34.1__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 (129) hide show
  1. siliconcompiler/__init__.py +23 -4
  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 +7 -6
  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/__init__.py +17 -0
  19. siliconcompiler/constraints/asic_component.py +378 -0
  20. siliconcompiler/constraints/asic_floorplan.py +449 -0
  21. siliconcompiler/constraints/asic_pins.py +489 -0
  22. siliconcompiler/constraints/asic_timing.py +517 -0
  23. siliconcompiler/core.py +10 -35
  24. siliconcompiler/data/templates/tcl/manifest.tcl.j2 +8 -0
  25. siliconcompiler/dependencyschema.py +96 -202
  26. siliconcompiler/design.py +327 -241
  27. siliconcompiler/filesetschema.py +250 -0
  28. siliconcompiler/flowgraph.py +298 -106
  29. siliconcompiler/fpga.py +124 -1
  30. siliconcompiler/library.py +331 -0
  31. siliconcompiler/metric.py +327 -92
  32. siliconcompiler/metrics/__init__.py +7 -0
  33. siliconcompiler/metrics/asic.py +245 -0
  34. siliconcompiler/metrics/fpga.py +220 -0
  35. siliconcompiler/package/__init__.py +391 -67
  36. siliconcompiler/package/git.py +92 -16
  37. siliconcompiler/package/github.py +114 -22
  38. siliconcompiler/package/https.py +79 -16
  39. siliconcompiler/packageschema.py +341 -16
  40. siliconcompiler/pathschema.py +255 -0
  41. siliconcompiler/pdk.py +566 -1
  42. siliconcompiler/project.py +1460 -0
  43. siliconcompiler/record.py +38 -1
  44. siliconcompiler/remote/__init__.py +5 -2
  45. siliconcompiler/remote/client.py +11 -6
  46. siliconcompiler/remote/schema.py +5 -23
  47. siliconcompiler/remote/server.py +41 -54
  48. siliconcompiler/report/__init__.py +3 -3
  49. siliconcompiler/report/dashboard/__init__.py +48 -14
  50. siliconcompiler/report/dashboard/cli/__init__.py +99 -21
  51. siliconcompiler/report/dashboard/cli/board.py +364 -179
  52. siliconcompiler/report/dashboard/web/__init__.py +90 -12
  53. siliconcompiler/report/dashboard/web/components/__init__.py +219 -240
  54. siliconcompiler/report/dashboard/web/components/flowgraph.py +49 -26
  55. siliconcompiler/report/dashboard/web/components/graph.py +139 -100
  56. siliconcompiler/report/dashboard/web/layouts/__init__.py +29 -1
  57. siliconcompiler/report/dashboard/web/layouts/_common.py +38 -2
  58. siliconcompiler/report/dashboard/web/layouts/vertical_flowgraph.py +39 -26
  59. siliconcompiler/report/dashboard/web/layouts/vertical_flowgraph_node_tab.py +50 -50
  60. siliconcompiler/report/dashboard/web/layouts/vertical_flowgraph_sac_tabs.py +49 -46
  61. siliconcompiler/report/dashboard/web/state.py +141 -14
  62. siliconcompiler/report/dashboard/web/utils/__init__.py +79 -16
  63. siliconcompiler/report/dashboard/web/utils/file_utils.py +74 -11
  64. siliconcompiler/report/dashboard/web/viewer.py +25 -1
  65. siliconcompiler/report/report.py +5 -2
  66. siliconcompiler/report/summary_image.py +29 -11
  67. siliconcompiler/scheduler/__init__.py +9 -1
  68. siliconcompiler/scheduler/docker.py +81 -4
  69. siliconcompiler/scheduler/run_node.py +37 -20
  70. siliconcompiler/scheduler/scheduler.py +211 -36
  71. siliconcompiler/scheduler/schedulernode.py +394 -60
  72. siliconcompiler/scheduler/send_messages.py +77 -29
  73. siliconcompiler/scheduler/slurm.py +76 -12
  74. siliconcompiler/scheduler/taskscheduler.py +142 -21
  75. siliconcompiler/schema/__init__.py +0 -4
  76. siliconcompiler/schema/baseschema.py +338 -59
  77. siliconcompiler/schema/editableschema.py +14 -6
  78. siliconcompiler/schema/journal.py +28 -17
  79. siliconcompiler/schema/namedschema.py +22 -14
  80. siliconcompiler/schema/parameter.py +89 -28
  81. siliconcompiler/schema/parametertype.py +2 -0
  82. siliconcompiler/schema/parametervalue.py +258 -15
  83. siliconcompiler/schema/safeschema.py +25 -2
  84. siliconcompiler/schema/schema_cfg.py +23 -19
  85. siliconcompiler/schema/utils.py +2 -2
  86. siliconcompiler/schema_obj.py +24 -5
  87. siliconcompiler/tool.py +1131 -265
  88. siliconcompiler/tools/bambu/__init__.py +41 -0
  89. siliconcompiler/tools/builtin/concatenate.py +2 -2
  90. siliconcompiler/tools/builtin/minimum.py +2 -1
  91. siliconcompiler/tools/builtin/mux.py +2 -1
  92. siliconcompiler/tools/builtin/nop.py +2 -1
  93. siliconcompiler/tools/builtin/verify.py +2 -1
  94. siliconcompiler/tools/klayout/__init__.py +95 -0
  95. siliconcompiler/tools/openroad/__init__.py +289 -0
  96. siliconcompiler/tools/openroad/scripts/apr/preamble.tcl +3 -0
  97. siliconcompiler/tools/openroad/scripts/apr/sc_detailed_route.tcl +7 -2
  98. siliconcompiler/tools/openroad/scripts/apr/sc_global_route.tcl +8 -4
  99. siliconcompiler/tools/openroad/scripts/apr/sc_init_floorplan.tcl +9 -5
  100. siliconcompiler/tools/openroad/scripts/common/write_images.tcl +5 -1
  101. siliconcompiler/tools/slang/__init__.py +1 -1
  102. siliconcompiler/tools/slang/elaborate.py +2 -1
  103. siliconcompiler/tools/vivado/scripts/sc_run.tcl +1 -1
  104. siliconcompiler/tools/vivado/scripts/sc_syn_fpga.tcl +8 -1
  105. siliconcompiler/tools/vivado/syn_fpga.py +6 -0
  106. siliconcompiler/tools/vivado/vivado.py +35 -2
  107. siliconcompiler/tools/vpr/__init__.py +150 -0
  108. siliconcompiler/tools/yosys/__init__.py +369 -1
  109. siliconcompiler/tools/yosys/scripts/procs.tcl +0 -1
  110. siliconcompiler/toolscripts/_tools.json +5 -10
  111. siliconcompiler/utils/__init__.py +66 -0
  112. siliconcompiler/utils/flowgraph.py +2 -2
  113. siliconcompiler/utils/issue.py +2 -1
  114. siliconcompiler/utils/logging.py +14 -0
  115. siliconcompiler/utils/multiprocessing.py +256 -0
  116. siliconcompiler/utils/showtools.py +10 -0
  117. {siliconcompiler-0.34.1.dist-info → siliconcompiler-0.34.3.dist-info}/METADATA +6 -6
  118. {siliconcompiler-0.34.1.dist-info → siliconcompiler-0.34.3.dist-info}/RECORD +122 -115
  119. {siliconcompiler-0.34.1.dist-info → siliconcompiler-0.34.3.dist-info}/entry_points.txt +3 -0
  120. siliconcompiler/schema/cmdlineschema.py +0 -250
  121. siliconcompiler/schema/packageschema.py +0 -101
  122. siliconcompiler/toolscripts/rhel8/install-slang.sh +0 -40
  123. siliconcompiler/toolscripts/rhel9/install-slang.sh +0 -40
  124. siliconcompiler/toolscripts/ubuntu20/install-slang.sh +0 -47
  125. siliconcompiler/toolscripts/ubuntu22/install-slang.sh +0 -37
  126. siliconcompiler/toolscripts/ubuntu24/install-slang.sh +0 -37
  127. {siliconcompiler-0.34.1.dist-info → siliconcompiler-0.34.3.dist-info}/WHEEL +0 -0
  128. {siliconcompiler-0.34.1.dist-info → siliconcompiler-0.34.3.dist-info}/licenses/LICENSE +0 -0
  129. {siliconcompiler-0.34.1.dist-info → siliconcompiler-0.34.3.dist-info}/top_level.txt +0 -0
@@ -4,7 +4,10 @@
4
4
  # SC dependencies outside of its directory, since it may be used by tool drivers
5
5
  # that have isolated Python environments.
6
6
 
7
+ import contextlib
7
8
  import copy
9
+ import importlib
10
+ import logging
8
11
 
9
12
  try:
10
13
  import gzip
@@ -21,7 +24,10 @@ except ModuleNotFoundError:
21
24
 
22
25
  import os.path
23
26
 
24
- from .parameter import Parameter
27
+ from functools import cache
28
+ from typing import Dict, Type, Tuple, Union, Set, Callable, List
29
+
30
+ from .parameter import Parameter, NodeValue
25
31
  from .journal import Journal
26
32
 
27
33
 
@@ -36,8 +42,100 @@ class BaseSchema:
36
42
  self.__default = None
37
43
  self.__journal = Journal()
38
44
  self.__parent = self
45
+ self.__active = None
46
+ self.__key = None
39
47
 
40
- def _from_dict(self, manifest, keypath, version=None):
48
+ @property
49
+ def _keypath(self):
50
+ '''
51
+ Returns the key to the current section of the schema
52
+ '''
53
+ if self.__parent is self:
54
+ return tuple()
55
+ return tuple([*self.__parent._keypath, self.__key])
56
+
57
+ @staticmethod
58
+ @cache
59
+ def __get_child_classes() -> Dict[str, Type["BaseSchema"]]:
60
+ """
61
+ Returns all known subclasses of BaseSchema
62
+ """
63
+ def recurse(cls):
64
+ subclss = set()
65
+ subclss.add(cls)
66
+ for subcls in cls.__subclasses__():
67
+ subclss.update(recurse(subcls))
68
+ return subclss
69
+
70
+ # Resolve true base
71
+ cls_mapping = {}
72
+ for cls in recurse(BaseSchema):
73
+ try:
74
+ cls_mapping.setdefault(cls._getdict_type(), set()).add(cls)
75
+ except NotImplementedError:
76
+ pass
77
+
78
+ # Build lookup table
79
+ cls_map = {}
80
+ for cls_type, clss in cls_mapping.items():
81
+ for cls in clss:
82
+ cls_map[f"{cls.__module__}/{cls.__name__}"] = cls
83
+
84
+ if len(clss) > 1:
85
+ found = False
86
+ for cls in clss:
87
+ if cls.__name__ == cls_type:
88
+ cls_map[cls_type] = cls
89
+ found = True
90
+ break
91
+ if not found:
92
+ raise RuntimeError(f"fatal error at: {cls_type}")
93
+ else:
94
+ cls_map[cls_type] = list(clss)[0]
95
+ return cls_map
96
+
97
+ @staticmethod
98
+ @cache
99
+ def __load_schema_class(cls_name: str) -> Type["BaseSchema"]:
100
+ """
101
+ Load a schema class from a string
102
+ """
103
+ try:
104
+ module_name, cls_name = cls_name.split("/")
105
+ except (ValueError, AttributeError):
106
+ return None
107
+
108
+ try:
109
+ module = importlib.import_module(module_name)
110
+ except (ImportError, ModuleNotFoundError, SyntaxError):
111
+ return None
112
+
113
+ cls = getattr(module, cls_name, None)
114
+ if not cls:
115
+ return None
116
+ if not issubclass(cls, BaseSchema):
117
+ raise TypeError(f"{cls_name} must be a BaseSchema type")
118
+ return cls
119
+
120
+ @staticmethod
121
+ def __process_meta_section(meta: Dict[str, str]) -> Type["BaseSchema"]:
122
+ """
123
+ Handle __meta__ section of the schema by loading the appropriate class
124
+ """
125
+ cls_map = BaseSchema.__get_child_classes()
126
+
127
+ # Lookup object, use class first, then type
128
+ cls_name = meta.get("class", None)
129
+ cls = None
130
+ if cls_name:
131
+ cls = cls_map.get(cls_name, None)
132
+ if not cls:
133
+ cls = BaseSchema.__load_schema_class(cls_name)
134
+ if not cls:
135
+ cls = cls_map.get(meta.get("sctype", None), None)
136
+ return cls
137
+
138
+ def _from_dict(self, manifest: Dict, keypath: Tuple[str], version: str = None):
41
139
  '''
42
140
  Decodes a dictionary into a schema object
43
141
 
@@ -52,6 +150,10 @@ class BaseSchema:
52
150
 
53
151
  if "__journal__" in manifest:
54
152
  self.__journal.from_dict(manifest["__journal__"])
153
+ del manifest["__journal__"]
154
+
155
+ if "__meta__" in manifest:
156
+ del manifest["__meta__"]
55
157
 
56
158
  if self.__default:
57
159
  data = manifest.get("default", None)
@@ -62,9 +164,25 @@ class BaseSchema:
62
164
 
63
165
  for key, data in manifest.items():
64
166
  obj = self.__manifest.get(key, None)
167
+ if not obj and isinstance(data, dict) and "__meta__" in data:
168
+ # Lookup object, use class first, then type
169
+ cls = BaseSchema.__process_meta_section(data["__meta__"])
170
+ if cls is BaseSchema and self.__default:
171
+ # Use default when BaseSchema is the class
172
+ obj = self.__default.copy(key=keypath + [key])
173
+ self.__manifest[key] = obj
174
+ elif cls:
175
+ # Create object and connect to schema
176
+ obj = cls()
177
+ obj.__parent = self
178
+ obj.__key = key
179
+ self.__manifest[key] = obj
180
+
181
+ # Use default if it is available
65
182
  if not obj and self.__default:
66
- obj = self.__default.copy()
183
+ obj = self.__default.copy(key=keypath + [key])
67
184
  self.__manifest[key] = obj
185
+
68
186
  if obj:
69
187
  obj._from_dict(data, keypath + [key], version=version)
70
188
  handled.add(key)
@@ -75,7 +193,7 @@ class BaseSchema:
75
193
 
76
194
  # Manifest methods
77
195
  @classmethod
78
- def from_manifest(cls, filepath=None, cfg=None):
196
+ def from_manifest(cls, filepath: str = None, cfg: Dict = None) -> "BaseSchema":
79
197
  '''
80
198
  Create a new schema based on the provided source files.
81
199
 
@@ -89,15 +207,24 @@ class BaseSchema:
89
207
  if not filepath and cfg is None:
90
208
  raise RuntimeError("filepath or dictionary is required")
91
209
 
92
- schema = cls()
93
210
  if filepath:
94
- schema.read_manifest(filepath)
95
- if cfg:
96
- schema._from_dict(cfg, [])
211
+ cfg = BaseSchema._read_manifest(filepath)
212
+
213
+ new_cls = None
214
+ if "__meta__" in cfg:
215
+ # Determine correct class
216
+ new_cls = BaseSchema.__process_meta_section(cfg["__meta__"])
217
+ if new_cls:
218
+ schema = new_cls()
219
+ else:
220
+ schema = cls()
221
+
222
+ schema._from_dict(cfg, [])
223
+
97
224
  return schema
98
225
 
99
226
  @staticmethod
100
- def __open_file(filepath, is_read=True):
227
+ def __open_file(filepath: str, is_read: bool = True):
101
228
  _, ext = os.path.splitext(filepath)
102
229
  if ext.lower() == ".gz":
103
230
  if not _has_gzip:
@@ -105,7 +232,27 @@ class BaseSchema:
105
232
  return gzip.open(filepath, mode="rt" if is_read else "wt", encoding="utf-8")
106
233
  return open(filepath, mode="r" if is_read else "w", encoding="utf-8")
107
234
 
108
- def read_manifest(self, filepath):
235
+ def __format_key(self, *key: str):
236
+ return f"[{','.join([*self._keypath, *key])}]"
237
+
238
+ @staticmethod
239
+ def _read_manifest(filepath: str):
240
+ """
241
+ Reads a manifest from disk and returns dictionary.
242
+
243
+ Args:
244
+ filename (path): Path to a manifest file to be loaded.
245
+ """
246
+
247
+ fin = BaseSchema.__open_file(filepath)
248
+ try:
249
+ manifest = json.loads(fin.read())
250
+ finally:
251
+ fin.close()
252
+
253
+ return manifest
254
+
255
+ def read_manifest(self, filepath: str):
109
256
  """
110
257
  Reads a manifest from disk and replaces the current data with the data in the file.
111
258
 
@@ -117,13 +264,9 @@ class BaseSchema:
117
264
  Loads the file mychip.json into the current Schema object.
118
265
  """
119
266
 
120
- fin = BaseSchema.__open_file(filepath)
121
- manifest = json.loads(fin.read())
122
- fin.close()
123
-
124
- self._from_dict(manifest, [])
267
+ self._from_dict(BaseSchema._read_manifest(filepath), [])
125
268
 
126
- def write_manifest(self, filepath):
269
+ def write_manifest(self, filepath: str):
127
270
  '''
128
271
  Writes the manifest to a file.
129
272
 
@@ -147,11 +290,11 @@ class BaseSchema:
147
290
 
148
291
  # Accessor methods
149
292
  def __search(self,
150
- *keypath,
151
- insert_defaults=False,
152
- use_default=False,
153
- require_leaf=True,
154
- complete_path=None):
293
+ *keypath: str,
294
+ insert_defaults: bool = False,
295
+ use_default: bool = False,
296
+ require_leaf: bool = True,
297
+ complete_path: bool = None) -> Union["BaseSchema", Parameter]:
155
298
  if len(keypath) == 0:
156
299
  if require_leaf:
157
300
  raise KeyError
@@ -189,7 +332,8 @@ class BaseSchema:
189
332
  complete_path=complete_path)
190
333
  return key_param
191
334
 
192
- def get(self, *keypath, field='value', step=None, index=None):
335
+ def get(self, *keypath: str, field: str = 'value',
336
+ step: str = None, index: Union[int, str] = None):
193
337
  """
194
338
  Returns a parameter field from the schema.
195
339
 
@@ -216,21 +360,23 @@ class BaseSchema:
216
360
 
217
361
  try:
218
362
  require_leaf = True
363
+ insert_defaults = False
219
364
  if field == 'schema':
220
365
  require_leaf = False
366
+ insert_defaults = True
221
367
  param = self.__search(
222
368
  *keypath,
223
- insert_defaults=False,
369
+ insert_defaults=insert_defaults,
224
370
  use_default=True,
225
371
  require_leaf=require_leaf)
226
372
  if field == 'schema':
227
373
  if isinstance(param, Parameter):
228
- raise ValueError(f"[{','.join(keypath)}] is a complete keypath")
374
+ raise ValueError(f"{self.__format_key(*keypath)} is a complete keypath")
229
375
  self.__journal.record("get", keypath, field=field, step=step, index=index)
230
376
  param.__journal = self.__journal.get_child(*keypath)
231
377
  return param
232
378
  except KeyError:
233
- raise KeyError(f"[{','.join(keypath)}] is not a valid keypath")
379
+ raise KeyError(f"{self.__format_key(*keypath)} is not a valid keypath")
234
380
  if field is None:
235
381
  return param
236
382
 
@@ -239,11 +385,12 @@ class BaseSchema:
239
385
  self.__journal.record("get", keypath, field=field, step=step, index=index)
240
386
  return get_ret
241
387
  except Exception as e:
242
- new_msg = f"error while accessing [{','.join(keypath)}]: {e.args[0]}"
388
+ new_msg = f"error while accessing {self.__format_key(*keypath)}: {e.args[0]}"
243
389
  e.args = (new_msg, *e.args[1:])
244
390
  raise e
245
391
 
246
- def set(self, *args, field='value', clobber=True, step=None, index=None):
392
+ def set(self, *args: str, field: str = 'value', clobber: bool = True,
393
+ step: str = None, index: Union[int, str] = None):
247
394
  '''
248
395
  Sets a schema parameter field.
249
396
 
@@ -273,7 +420,7 @@ class BaseSchema:
273
420
  try:
274
421
  param = self.__search(*keypath, insert_defaults=True)
275
422
  except KeyError:
276
- raise KeyError(f"[{','.join(keypath)}] is not a valid keypath")
423
+ raise KeyError(f"{self.__format_key(*keypath)} is not a valid keypath")
277
424
 
278
425
  try:
279
426
  set_ret = param.set(value, field=field, clobber=clobber,
@@ -281,13 +428,15 @@ class BaseSchema:
281
428
  if set_ret:
282
429
  self.__journal.record("set", keypath, value=value, field=field,
283
430
  step=step, index=index)
431
+ self.__process_active(param, set_ret)
284
432
  return set_ret
285
433
  except Exception as e:
286
- new_msg = f"error while setting [{','.join(keypath)}]: {e.args[0]}"
434
+ new_msg = f"error while setting {self.__format_key(*keypath)}: {e.args[0]}"
287
435
  e.args = (new_msg, *e.args[1:])
288
436
  raise e
289
437
 
290
- def add(self, *args, field='value', step=None, index=None):
438
+ def add(self, *args: str, field: str = 'value',
439
+ step: str = None, index: Union[int, str] = None):
291
440
  '''
292
441
  Adds item(s) to a schema parameter list.
293
442
 
@@ -316,20 +465,21 @@ class BaseSchema:
316
465
  try:
317
466
  param = self.__search(*keypath, insert_defaults=True)
318
467
  except KeyError:
319
- raise KeyError(f"[{','.join(keypath)}] is not a valid keypath")
468
+ raise KeyError(f"{self.__format_key(*keypath)} is not a valid keypath")
320
469
 
321
470
  try:
322
471
  add_ret = param.add(value, field=field, step=step, index=index)
323
472
  if add_ret:
324
473
  self.__journal.record("add", keypath, value=value, field=field,
325
474
  step=step, index=index)
475
+ self.__process_active(param, add_ret)
326
476
  return add_ret
327
477
  except Exception as e:
328
- new_msg = f"error while adding to [{','.join(keypath)}]: {e.args[0]}"
478
+ new_msg = f"error while adding to {self.__format_key(*keypath)}: {e.args[0]}"
329
479
  e.args = (new_msg, *e.args[1:])
330
480
  raise e
331
481
 
332
- def unset(self, *keypath, step=None, index=None):
482
+ def unset(self, *keypath: str, step: str = None, index: Union[int, str] = None):
333
483
  '''
334
484
  Unsets a schema parameter.
335
485
 
@@ -358,17 +508,17 @@ class BaseSchema:
358
508
  try:
359
509
  param = self.__search(*keypath, use_default=True)
360
510
  except KeyError:
361
- raise KeyError(f"[{','.join(keypath)}] is not a valid keypath")
511
+ raise KeyError(f"{self.__format_key(*keypath)} is not a valid keypath")
362
512
 
363
513
  try:
364
514
  param.unset(step=step, index=index)
365
515
  self.__journal.record("unset", keypath, step=step, index=index)
366
516
  except Exception as e:
367
- new_msg = f"error while unsetting [{','.join(keypath)}]: {e.args[0]}"
517
+ new_msg = f"error while unsetting {self.__format_key(*keypath)}: {e.args[0]}"
368
518
  e.args = (new_msg, *e.args[1:])
369
519
  raise e
370
520
 
371
- def remove(self, *keypath):
521
+ def remove(self, *keypath: str):
372
522
  '''
373
523
  Remove a schema parameter and its subparameters.
374
524
 
@@ -384,7 +534,7 @@ class BaseSchema:
384
534
  try:
385
535
  key_param = self.__search(*search_path, require_leaf=False)
386
536
  except KeyError:
387
- raise KeyError(f"[{','.join(keypath)}] is not a valid keypath")
537
+ raise KeyError(f"{self.__format_key(*keypath)} is not a valid keypath")
388
538
 
389
539
  if removal_key not in key_param.__manifest:
390
540
  return
@@ -398,7 +548,8 @@ class BaseSchema:
398
548
  del key_param.__manifest[removal_key]
399
549
  self.__journal.record("remove", keypath)
400
550
 
401
- def valid(self, *keypath, default_valid=False, check_complete=False):
551
+ def valid(self, *keypath: str, default_valid: bool = False,
552
+ check_complete: bool = False) -> bool:
402
553
  """
403
554
  Checks validity of a keypath.
404
555
 
@@ -432,7 +583,7 @@ class BaseSchema:
432
583
  return isinstance(param, Parameter)
433
584
  return True
434
585
 
435
- def getkeys(self, *keypath):
586
+ def getkeys(self, *keypath: str) -> Tuple[str]:
436
587
  """
437
588
  Returns a tuple of schema dictionary keys.
438
589
 
@@ -454,15 +605,15 @@ class BaseSchema:
454
605
  try:
455
606
  key_param = self.__search(*keypath, require_leaf=False)
456
607
  except KeyError:
457
- raise KeyError(f"[{','.join(keypath)}] is not a valid keypath")
608
+ raise KeyError(f"{self.__format_key(*keypath)} is not a valid keypath")
458
609
  if isinstance(key_param, Parameter):
459
610
  return tuple()
460
611
  else:
461
612
  key_param = self
462
613
 
463
- return tuple(key_param.__manifest.keys())
614
+ return tuple(sorted(key_param.__manifest.keys()))
464
615
 
465
- def allkeys(self, *keypath, include_default=True):
616
+ def allkeys(self, *keypath: str, include_default: bool = True) -> Set[Tuple[str]]:
466
617
  '''
467
618
  Returns all keypaths in the schema as a set of tuples.
468
619
 
@@ -491,7 +642,16 @@ class BaseSchema:
491
642
  add(keys, key, item)
492
643
  return set(keys)
493
644
 
494
- def getdict(self, *keypath, include_default=True, values_only=False):
645
+ @classmethod
646
+ def _getdict_type(cls) -> str:
647
+ """
648
+ Returns the meta data for getdict
649
+ """
650
+
651
+ return "BaseSchema"
652
+
653
+ def getdict(self, *keypath: str, include_default: bool = True,
654
+ values_only: bool = False) -> Dict:
495
655
  """
496
656
  Returns a schema dictionary.
497
657
 
@@ -534,10 +694,19 @@ class BaseSchema:
534
694
  if not values_only and self.__journal.has_journaling():
535
695
  manifest["__journal__"] = self.__journal.get()
536
696
 
697
+ if not values_only and self.__class__ is not BaseSchema:
698
+ manifest["__meta__"] = {
699
+ "class": f"{self.__class__.__module__}/{self.__class__.__name__}"
700
+ }
701
+ try:
702
+ manifest["__meta__"]["sctype"] = self._getdict_type()
703
+ except NotImplementedError:
704
+ pass
705
+
537
706
  return manifest
538
707
 
539
708
  # Utility functions
540
- def copy(self, key=None):
709
+ def copy(self, key: Tuple[str] = None):
541
710
  """
542
711
  Returns a copy of this schema.
543
712
 
@@ -555,10 +724,38 @@ class BaseSchema:
555
724
  else:
556
725
  schema_copy.__parent = schema_copy
557
726
 
727
+ if key:
728
+ schema_copy.__key = key[-1]
729
+
558
730
  return schema_copy
559
731
 
560
- def find_files(self, *keypath, missing_ok=False, step=None, index=None,
561
- packages=None, collection_dir=None, cwd=None):
732
+ def _find_files_search_paths(self, keypath: Tuple[str], step: str, index: str):
733
+ """
734
+ Returns a list of paths to search during find files.
735
+
736
+ Args:
737
+ keypath (str): final component of keypath
738
+ step (str): Step name.
739
+ index (str): Index name.
740
+ """
741
+ return []
742
+
743
+ def _find_files_dataroot_resolvers(self):
744
+ """
745
+ Returns a dictionary of path resolevrs data directory handling for find_files
746
+
747
+ Returns:
748
+ dictionary of str to resolver mapping
749
+ """
750
+ if self.__parent is self:
751
+ return {}
752
+ return self.__parent._find_files_dataroot_resolvers()
753
+
754
+ def find_files(self, *keypath: str, missing_ok: bool = False,
755
+ step: str = None, index: Union[int, str] = None,
756
+ packages: Dict[str, Union[str, Callable]] = None,
757
+ collection_dir: str = None,
758
+ cwd: str = None) -> Union[str, List[str], Set[str]]:
562
759
  """
563
760
  Returns absolute paths to files or directories based on the keypath
564
761
  provided.
@@ -592,10 +789,13 @@ class BaseSchema:
592
789
  the schema.
593
790
  """
594
791
 
595
- param = self.get(*keypath, field=None)
792
+ base_schema = self.get(*keypath[0:-1], field="schema")
793
+
794
+ param = base_schema.get(keypath[-1], field=None)
596
795
  paramtype = param.get(field='type')
597
796
  if 'file' not in paramtype and 'dir' not in paramtype:
598
- raise TypeError(f'Cannot find files on [{",".join(keypath)}], must be a path type')
797
+ raise TypeError(
798
+ f'Cannot find files on {self.__format_key(*keypath)}, must be a path type')
599
799
 
600
800
  paths = param.get(field=None, step=step, index=index)
601
801
 
@@ -615,23 +815,26 @@ class BaseSchema:
615
815
  cwd = os.getcwd()
616
816
 
617
817
  if packages is None:
618
- packages = {}
818
+ packages = base_schema._find_files_dataroot_resolvers()
619
819
 
620
820
  resolved_paths = []
821
+ root_search_paths = base_schema._find_files_search_paths(keypath[-1], step, index)
621
822
  for path in paths:
622
- search_paths = []
823
+ search_paths = root_search_paths.copy()
623
824
 
624
825
  package = path.get(field="package")
625
826
  if package:
626
827
  if package not in packages:
627
- raise ValueError(f"Resolver for {package} not provided")
828
+ raise ValueError(f"Resolver for {package} not provided: "
829
+ f"{self.__format_key(*keypath)}")
628
830
  package_path = packages[package]
629
831
  if isinstance(package_path, str):
630
832
  search_paths.append(os.path.abspath(package_path))
631
833
  elif callable(package_path):
632
834
  search_paths.append(package_path())
633
835
  else:
634
- raise TypeError(f"Resolver for {package} is not a recognized type")
836
+ raise TypeError(f"Resolver for {package} is not a recognized type: "
837
+ f"{self.__format_key(*keypath)}")
635
838
  else:
636
839
  if cwd:
637
840
  search_paths.append(os.path.abspath(cwd))
@@ -644,10 +847,11 @@ class BaseSchema:
644
847
  if not missing_ok:
645
848
  if package:
646
849
  raise FileNotFoundError(
647
- f'Could not find "{path.get()}" in {package} [{",".join(keypath)}]')
850
+ f'Could not find "{path.get()}" in {package} '
851
+ f'{self.__format_key(*keypath)}')
648
852
  else:
649
853
  raise FileNotFoundError(
650
- f'Could not find "{path.get()}" [{",".join(keypath)}]')
854
+ f'Could not find "{path.get()}" {self.__format_key(*keypath)}')
651
855
  resolved_paths.append(resolved_path)
652
856
 
653
857
  if not is_list:
@@ -656,8 +860,11 @@ class BaseSchema:
656
860
  return resolved_paths[0]
657
861
  return resolved_paths
658
862
 
659
- def check_filepaths(self, ignore_keys=None, logger=None,
660
- packages=None, collection_dir=None, cwd=None):
863
+ def check_filepaths(self, ignore_keys: bool = None,
864
+ logger: logging.Logger = None,
865
+ packages: Dict[str, Union[str, Callable]] = None,
866
+ collection_dir: str = None,
867
+ cwd: str = None) -> bool:
661
868
  '''
662
869
  Verifies that paths to all files in manifest are valid.
663
870
 
@@ -717,12 +924,12 @@ class BaseSchema:
717
924
  else:
718
925
  node_indicator = f" ({step}/{index})"
719
926
 
720
- logger.error(f"Parameter [{','.join(keypath)}]{node_indicator} path "
721
- f"{check_file} is invalid")
927
+ logger.error(f"Parameter {self.__format_key(*keypath)}{node_indicator} "
928
+ f"path {check_file} is invalid")
722
929
 
723
930
  return not error
724
931
 
725
- def _parent(self, root=False):
932
+ def _parent(self, root: bool = False) -> "BaseSchema":
726
933
  '''
727
934
  Returns the parent of this schema section, if root is true the root parent
728
935
  will be returned.
@@ -735,3 +942,75 @@ class BaseSchema:
735
942
  if self.__parent is self:
736
943
  return self
737
944
  return self.__parent._parent(root=root)
945
+
946
+ @contextlib.contextmanager
947
+ def _active(self, **kwargs):
948
+ '''
949
+ Use this context to temporarily set additional fields in :meth:`.set` and :meth:`.add`.
950
+ Additional fields can be specified which can be accessed by :meth:`._get_active`.
951
+
952
+ Args:
953
+ kwargs (dict of str): keyword arguments that are used for setting values
954
+
955
+ Example:
956
+ >>> with schema._active(package="lambdalib"):
957
+ ... schema.set("file", "top.v")
958
+ Sets the file to top.v and associates lambdalib as the package.
959
+ '''
960
+ if self.__active:
961
+ orig_active = self.__active.copy()
962
+ else:
963
+ orig_active = None
964
+
965
+ if self.__active is None:
966
+ self.__active = {}
967
+
968
+ self.__active.update(kwargs)
969
+ try:
970
+ yield
971
+ finally:
972
+ self.__active = orig_active
973
+
974
+ def _get_active(self, field: str, defvalue=None):
975
+ '''
976
+ Get the value of a specific field.
977
+
978
+ Args:
979
+ field (str): if None, return the current active dictionary,
980
+ otherwise the value, if the field is not present, defvalue is returned.
981
+ defvalue (any): value to return if the field is not present.
982
+ '''
983
+ if self.__active is None:
984
+ return defvalue
985
+
986
+ if field is None:
987
+ return self.__active.copy()
988
+
989
+ return self.__active.get(field, defvalue)
990
+
991
+ def __process_active(self, param, nodevalues):
992
+ if not self.__active:
993
+ return
994
+
995
+ if not isinstance(nodevalues, (list, set, tuple)):
996
+ # Make everything a list
997
+ nodevalues = [nodevalues]
998
+
999
+ if not all([isinstance(v, NodeValue) for v in nodevalues]):
1000
+ nodevalues = []
1001
+ nodevalues_fields = []
1002
+ else:
1003
+ nodevalues_fields = nodevalues[0].fields
1004
+
1005
+ key_fields = ("copy", "lock")
1006
+
1007
+ for field, value in self.__active.items():
1008
+ if field in key_fields:
1009
+ param.set(value, field=field)
1010
+ continue
1011
+
1012
+ if field not in nodevalues_fields:
1013
+ continue
1014
+
1015
+ for param in nodevalues:
1016
+ param.set(value, field=field)