siliconcompiler 0.32.3__py3-none-any.whl → 0.33.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 (154) hide show
  1. siliconcompiler/__init__.py +19 -2
  2. siliconcompiler/_metadata.py +1 -1
  3. siliconcompiler/apps/sc.py +2 -2
  4. siliconcompiler/apps/sc_install.py +3 -3
  5. siliconcompiler/apps/sc_issue.py +1 -1
  6. siliconcompiler/apps/sc_remote.py +4 -4
  7. siliconcompiler/apps/sc_show.py +2 -2
  8. siliconcompiler/apps/utils/replay.py +5 -3
  9. siliconcompiler/asic.py +120 -0
  10. siliconcompiler/checklist.py +150 -0
  11. siliconcompiler/core.py +267 -289
  12. siliconcompiler/flowgraph.py +803 -515
  13. siliconcompiler/fpga.py +84 -0
  14. siliconcompiler/metric.py +420 -0
  15. siliconcompiler/optimizer/vizier.py +2 -3
  16. siliconcompiler/package/__init__.py +29 -6
  17. siliconcompiler/pdk.py +415 -0
  18. siliconcompiler/record.py +449 -0
  19. siliconcompiler/remote/client.py +6 -3
  20. siliconcompiler/remote/schema.py +116 -112
  21. siliconcompiler/remote/server.py +3 -5
  22. siliconcompiler/report/dashboard/cli/__init__.py +13 -722
  23. siliconcompiler/report/dashboard/cli/board.py +895 -0
  24. siliconcompiler/report/dashboard/web/__init__.py +10 -10
  25. siliconcompiler/report/dashboard/web/components/__init__.py +5 -4
  26. siliconcompiler/report/dashboard/web/components/flowgraph.py +3 -3
  27. siliconcompiler/report/dashboard/web/components/graph.py +6 -3
  28. siliconcompiler/report/dashboard/web/state.py +1 -1
  29. siliconcompiler/report/dashboard/web/utils/__init__.py +4 -3
  30. siliconcompiler/report/html_report.py +2 -3
  31. siliconcompiler/report/report.py +13 -7
  32. siliconcompiler/report/summary_image.py +1 -1
  33. siliconcompiler/report/summary_table.py +3 -3
  34. siliconcompiler/report/utils.py +11 -10
  35. siliconcompiler/scheduler/__init__.py +145 -280
  36. siliconcompiler/scheduler/run_node.py +2 -1
  37. siliconcompiler/scheduler/send_messages.py +4 -4
  38. siliconcompiler/scheduler/slurm.py +2 -2
  39. siliconcompiler/schema/__init__.py +19 -2
  40. siliconcompiler/schema/baseschema.py +493 -0
  41. siliconcompiler/schema/cmdlineschema.py +250 -0
  42. siliconcompiler/{sphinx_ext → schema/docs}/__init__.py +3 -1
  43. siliconcompiler/{sphinx_ext → schema/docs}/dynamicgen.py +63 -81
  44. siliconcompiler/{sphinx_ext → schema/docs}/schemagen.py +73 -85
  45. siliconcompiler/{sphinx_ext → schema/docs}/utils.py +12 -13
  46. siliconcompiler/schema/editableschema.py +136 -0
  47. siliconcompiler/schema/journalingschema.py +238 -0
  48. siliconcompiler/schema/namedschema.py +41 -0
  49. siliconcompiler/schema/packageschema.py +101 -0
  50. siliconcompiler/schema/parameter.py +791 -0
  51. siliconcompiler/schema/parametertype.py +323 -0
  52. siliconcompiler/schema/parametervalue.py +736 -0
  53. siliconcompiler/schema/safeschema.py +37 -0
  54. siliconcompiler/schema/schema_cfg.py +109 -1789
  55. siliconcompiler/schema/utils.py +5 -68
  56. siliconcompiler/schema_obj.py +119 -0
  57. siliconcompiler/tool.py +1308 -0
  58. siliconcompiler/tools/_common/__init__.py +6 -10
  59. siliconcompiler/tools/_common/sdc/sc_constraints.sdc +1 -1
  60. siliconcompiler/tools/bluespec/convert.py +7 -7
  61. siliconcompiler/tools/builtin/_common.py +1 -1
  62. siliconcompiler/tools/builtin/concatenate.py +2 -2
  63. siliconcompiler/tools/builtin/minimum.py +1 -1
  64. siliconcompiler/tools/builtin/mux.py +2 -1
  65. siliconcompiler/tools/builtin/nop.py +1 -1
  66. siliconcompiler/tools/builtin/verify.py +6 -4
  67. siliconcompiler/tools/chisel/convert.py +4 -4
  68. siliconcompiler/tools/genfasm/bitstream.py +3 -3
  69. siliconcompiler/tools/ghdl/convert.py +1 -1
  70. siliconcompiler/tools/icarus/compile.py +4 -4
  71. siliconcompiler/tools/icepack/bitstream.py +6 -1
  72. siliconcompiler/tools/klayout/convert_drc_db.py +5 -0
  73. siliconcompiler/tools/klayout/klayout_export.py +0 -1
  74. siliconcompiler/tools/klayout/klayout_utils.py +3 -10
  75. siliconcompiler/tools/nextpnr/apr.py +6 -1
  76. siliconcompiler/tools/nextpnr/nextpnr.py +4 -4
  77. siliconcompiler/tools/openroad/_apr.py +13 -0
  78. siliconcompiler/tools/openroad/rdlroute.py +3 -3
  79. siliconcompiler/tools/openroad/scripts/apr/postamble.tcl +1 -1
  80. siliconcompiler/tools/openroad/scripts/apr/preamble.tcl +5 -5
  81. siliconcompiler/tools/openroad/scripts/apr/sc_antenna_repair.tcl +2 -2
  82. siliconcompiler/tools/openroad/scripts/apr/sc_clock_tree_synthesis.tcl +2 -2
  83. siliconcompiler/tools/openroad/scripts/apr/sc_detailed_placement.tcl +2 -2
  84. siliconcompiler/tools/openroad/scripts/apr/sc_detailed_route.tcl +2 -2
  85. siliconcompiler/tools/openroad/scripts/apr/sc_endcap_tapcell_insertion.tcl +2 -2
  86. siliconcompiler/tools/openroad/scripts/apr/sc_fillercell_insertion.tcl +2 -2
  87. siliconcompiler/tools/openroad/scripts/apr/sc_fillmetal_insertion.tcl +2 -2
  88. siliconcompiler/tools/openroad/scripts/apr/sc_global_placement.tcl +2 -2
  89. siliconcompiler/tools/openroad/scripts/apr/sc_global_route.tcl +2 -2
  90. siliconcompiler/tools/openroad/scripts/apr/sc_init_floorplan.tcl +2 -2
  91. siliconcompiler/tools/openroad/scripts/apr/sc_macro_placement.tcl +3 -3
  92. siliconcompiler/tools/openroad/scripts/apr/sc_metrics.tcl +2 -2
  93. siliconcompiler/tools/openroad/scripts/apr/sc_pin_placement.tcl +2 -2
  94. siliconcompiler/tools/openroad/scripts/apr/sc_power_grid.tcl +2 -2
  95. siliconcompiler/tools/openroad/scripts/apr/sc_repair_design.tcl +2 -2
  96. siliconcompiler/tools/openroad/scripts/apr/sc_repair_timing.tcl +2 -2
  97. siliconcompiler/tools/openroad/scripts/apr/sc_write_data.tcl +2 -2
  98. siliconcompiler/tools/openroad/scripts/common/procs.tcl +57 -1
  99. siliconcompiler/tools/openroad/scripts/common/screenshot.tcl +2 -2
  100. siliconcompiler/tools/openroad/scripts/common/write_images.tcl +28 -3
  101. siliconcompiler/tools/openroad/scripts/sc_rcx.tcl +1 -1
  102. siliconcompiler/tools/openroad/scripts/sc_rdlroute.tcl +3 -3
  103. siliconcompiler/tools/openroad/scripts/sc_show.tcl +6 -6
  104. siliconcompiler/tools/slang/__init__.py +10 -10
  105. siliconcompiler/tools/surelog/parse.py +4 -4
  106. siliconcompiler/tools/sv2v/convert.py +20 -3
  107. siliconcompiler/tools/verilator/compile.py +2 -2
  108. siliconcompiler/tools/verilator/verilator.py +3 -3
  109. siliconcompiler/tools/vpr/place.py +1 -1
  110. siliconcompiler/tools/vpr/route.py +4 -4
  111. siliconcompiler/tools/vpr/screenshot.py +1 -1
  112. siliconcompiler/tools/vpr/show.py +5 -5
  113. siliconcompiler/tools/vpr/vpr.py +24 -24
  114. siliconcompiler/tools/xdm/convert.py +2 -2
  115. siliconcompiler/tools/xyce/simulate.py +1 -1
  116. siliconcompiler/tools/yosys/sc_synth_asic.tcl +74 -68
  117. siliconcompiler/tools/yosys/syn_asic.py +2 -2
  118. siliconcompiler/toolscripts/_tools.json +7 -7
  119. siliconcompiler/toolscripts/ubuntu22/install-vpr.sh +0 -2
  120. siliconcompiler/toolscripts/ubuntu24/install-vpr.sh +0 -2
  121. siliconcompiler/utils/__init__.py +8 -112
  122. siliconcompiler/utils/flowgraph.py +339 -0
  123. siliconcompiler/{issue.py → utils/issue.py} +4 -3
  124. siliconcompiler/utils/logging.py +1 -2
  125. {siliconcompiler-0.32.3.dist-info → siliconcompiler-0.33.0.dist-info}/METADATA +9 -8
  126. {siliconcompiler-0.32.3.dist-info → siliconcompiler-0.33.0.dist-info}/RECORD +151 -134
  127. {siliconcompiler-0.32.3.dist-info → siliconcompiler-0.33.0.dist-info}/WHEEL +1 -1
  128. {siliconcompiler-0.32.3.dist-info → siliconcompiler-0.33.0.dist-info}/entry_points.txt +8 -8
  129. siliconcompiler/schema/schema_obj.py +0 -1936
  130. siliconcompiler/toolscripts/ubuntu20/install-vpr.sh +0 -29
  131. siliconcompiler/toolscripts/ubuntu20/install-yosys-parmys.sh +0 -61
  132. /siliconcompiler/{templates → data/templates}/__init__.py +0 -0
  133. /siliconcompiler/{templates → data/templates}/email/__init__.py +0 -0
  134. /siliconcompiler/{templates → data/templates}/email/general.j2 +0 -0
  135. /siliconcompiler/{templates → data/templates}/email/summary.j2 +0 -0
  136. /siliconcompiler/{templates → data/templates}/issue/README.txt +0 -0
  137. /siliconcompiler/{templates → data/templates}/issue/__init__.py +0 -0
  138. /siliconcompiler/{templates → data/templates}/issue/run.sh +0 -0
  139. /siliconcompiler/{templates → data/templates}/replay/replay.py.j2 +0 -0
  140. /siliconcompiler/{templates → data/templates}/replay/replay.sh.j2 +0 -0
  141. /siliconcompiler/{templates → data/templates}/replay/requirements.txt +0 -0
  142. /siliconcompiler/{templates → data/templates}/replay/setup.sh +0 -0
  143. /siliconcompiler/{templates → data/templates}/report/__init__.py +0 -0
  144. /siliconcompiler/{templates → data/templates}/report/bootstrap.min.css +0 -0
  145. /siliconcompiler/{templates → data/templates}/report/bootstrap.min.js +0 -0
  146. /siliconcompiler/{templates → data/templates}/report/bootstrap_LICENSE.md +0 -0
  147. /siliconcompiler/{templates → data/templates}/report/sc_report.j2 +0 -0
  148. /siliconcompiler/{templates → data/templates}/slurm/__init__.py +0 -0
  149. /siliconcompiler/{templates → data/templates}/slurm/run.sh +0 -0
  150. /siliconcompiler/{templates → data/templates}/tcl/__init__.py +0 -0
  151. /siliconcompiler/{templates → data/templates}/tcl/manifest.tcl.j2 +0 -0
  152. /siliconcompiler/{units.py → utils/units.py} +0 -0
  153. {siliconcompiler-0.32.3.dist-info → siliconcompiler-0.33.0.dist-info}/licenses/LICENSE +0 -0
  154. {siliconcompiler-0.32.3.dist-info → siliconcompiler-0.33.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,736 @@
1
+ import copy
2
+ import hashlib
3
+ import os
4
+ import pathlib
5
+
6
+ import os.path
7
+
8
+ from .parametertype import NodeType
9
+
10
+ try:
11
+ from base64 import b64encode, b64decode
12
+ from hashlib import blake2b
13
+ _has_sign = True
14
+ except: # noqa E722
15
+ _has_sign = False
16
+
17
+
18
+ class NodeListValue:
19
+ '''
20
+ Holds the data for a list schema type.
21
+
22
+ Args:
23
+ base (:class:`NodeValue`): base type for this list.
24
+ '''
25
+
26
+ def __init__(self, base):
27
+ self.__base = base
28
+ self.__values = []
29
+
30
+ def getdict(self):
31
+ """
32
+ Returns a schema dictionary.
33
+
34
+ Examples:
35
+ >>> value.getdict()
36
+ Returns the complete dictionary for the value
37
+ """
38
+
39
+ manifest = {}
40
+ for field in self.fields:
41
+ if field is None:
42
+ continue
43
+
44
+ value = self.get(field=field)
45
+ manifest.setdefault(field, []).extend(value)
46
+
47
+ if field == "author":
48
+ tmplist = []
49
+ for a in manifest[field]:
50
+ tmplist.extend(a)
51
+ manifest[field] = tmplist
52
+ return manifest
53
+
54
+ def _from_dict(self, manifest, keypath, version):
55
+ '''
56
+ Create a new value based on the provided dictionary.
57
+
58
+ Args:
59
+ manifest (dict): Manifest to decide.
60
+ keypath (list of str): Path to the current keypath.
61
+ version (packaging.Version): Version of the dictionary schema
62
+ sctype (str): schema type for this value
63
+ '''
64
+
65
+ self.__values.clear()
66
+ for n in range(len(manifest["value"])):
67
+ param = self.__base.copy()
68
+ self.__values.append(param)
69
+
70
+ for field in self.fields:
71
+ if field is None:
72
+ continue
73
+
74
+ if len(manifest[field]) <= n:
75
+ continue
76
+ param.set(manifest[field][n], field=field)
77
+
78
+ def get(self, field='value'):
79
+ """
80
+ Returns the value in the specified field
81
+
82
+ Args:
83
+ field (str): name of schema field.
84
+ """
85
+
86
+ vals = []
87
+ for val in self.__values:
88
+ value = val.get(field=field)
89
+ vals.append(value)
90
+ return vals
91
+
92
+ def set(self, value, field='value'):
93
+ """
94
+ Sets the value in a specific field and ensures it has been normalized.
95
+
96
+ Returns:
97
+ tuple of modified values
98
+
99
+ Args:
100
+ value (any): value to set
101
+ field (str): field to set
102
+ """
103
+
104
+ value = NodeType.normalize(value, self.type)
105
+
106
+ if field == 'value':
107
+ self.__values.clear()
108
+ else:
109
+ if len(value) != len(self.__values):
110
+ raise ValueError(f"set on {field} field must match number of values")
111
+
112
+ modified = list()
113
+ for n in range(len(value)):
114
+ if field == 'value':
115
+ self.__values.append(self.__base.copy())
116
+ self.__values[n].set(value[n], field=field)
117
+ modified.append(self.__values[n])
118
+ return tuple(modified)
119
+
120
+ def add(self, value, field='value'):
121
+ """
122
+ Adds the value in a specific field and ensures it has been normalized.
123
+
124
+ Returns:
125
+ tuple of modified values
126
+
127
+ Args:
128
+ value (any): value to set
129
+ field (str): field to set
130
+ """
131
+
132
+ modified = list()
133
+ if field == 'value':
134
+ value = NodeType.normalize(value, self.type)
135
+
136
+ for n in range(len(value)):
137
+ self.__values.append(self.__base.copy())
138
+ self.__values[-1].set(value[n], field=field)
139
+ modified.append(self.__values[-1])
140
+ else:
141
+ for val in self.__values:
142
+ val.add(value, field=field)
143
+ modified.append(val)
144
+ return tuple(modified)
145
+
146
+ @property
147
+ def fields(self):
148
+ """
149
+ Returns a list of valid fields for this value
150
+ """
151
+ return self.__base.fields
152
+
153
+ @property
154
+ def values(self):
155
+ '''
156
+ Returns a copy of the values stored in the list
157
+ '''
158
+ return self.__values.copy()
159
+
160
+ def copy(self):
161
+ """
162
+ Returns a copy of this value.
163
+ """
164
+
165
+ return copy.deepcopy(self)
166
+
167
+ def _set_type(self, sctype):
168
+ sctype = NodeType.parse(sctype)[0]
169
+ self.__base._set_type(sctype)
170
+ for val in self.__values:
171
+ val._set_type(sctype)
172
+
173
+ @property
174
+ def type(self):
175
+ """
176
+ Returns the type for this value
177
+ """
178
+ return [self.__base.type]
179
+
180
+
181
+ class NodeValue:
182
+ '''
183
+ Holds the data for a parameter.
184
+
185
+ Args:
186
+ sctype (str): type for this value
187
+ value (any): default value for this parameter
188
+ '''
189
+
190
+ def __init__(self, sctype, value=None):
191
+ self._set_type(sctype)
192
+ self.__value = value
193
+ self.__signature = None
194
+
195
+ @classmethod
196
+ def from_dict(cls, manifest, keypath, version, sctype):
197
+ '''
198
+ Create a new value based on the provided dictionary.
199
+
200
+ Args:
201
+ manifest (dict): Manifest to decide.
202
+ keypath (list of str): Path to the current keypath.
203
+ version (packaging.Version): Version of the dictionary schema
204
+ sctype (str): schema type for this value
205
+ '''
206
+
207
+ # create a dummy value
208
+ nodeval = cls(sctype)
209
+ nodeval._from_dict(manifest, keypath, version)
210
+ return nodeval
211
+
212
+ def getdict(self):
213
+ """
214
+ Returns a schema dictionary.
215
+
216
+ Examples:
217
+ >>> value.getdict()
218
+ Returns the complete dictionary for the value
219
+ """
220
+
221
+ return {
222
+ "value": self.get(field="value"),
223
+ "signature": self.get(field="signature")
224
+ }
225
+
226
+ def _from_dict(self, manifest, keypath, version):
227
+ '''
228
+ Copies the information from the manifest into this value.
229
+
230
+ Args:
231
+ manifest (dict): Manifest to decide.
232
+ keypath (list of str): Path to the current keypath.
233
+ version (packaging.Version): Version of the dictionary schema
234
+ '''
235
+
236
+ self.set(manifest["value"], field="value")
237
+ self.set(manifest["signature"], field="signature")
238
+
239
+ def get(self, field='value'):
240
+ """
241
+ Returns the value in the specified field
242
+
243
+ Args:
244
+ field (str): name of schema field.
245
+ """
246
+ if field is None:
247
+ return self
248
+ if field == 'value':
249
+ return copy.deepcopy(self.__value)
250
+ if field == 'signature':
251
+ return self.__signature
252
+ raise ValueError(f"{field} is not a valid field")
253
+
254
+ def set(self, value, field='value'):
255
+ """
256
+ Sets the value in a specific field and ensures it has been normalized.
257
+
258
+ Returns:
259
+ self
260
+
261
+ Args:
262
+ value (any): value to set
263
+ field (str): field to set
264
+ """
265
+ if field == 'value':
266
+ self.__value = NodeType.normalize(value, self.type)
267
+ return self
268
+ if field == 'signature':
269
+ self.__signature = NodeType.normalize(value, "str")
270
+ return self
271
+ raise ValueError(f"{field} is not a valid field")
272
+
273
+ def add(self, value, field='value'):
274
+ """
275
+ Not valid for this datatype, will raise a ValueError
276
+ """
277
+ raise ValueError(f"cannot add to {field} field")
278
+
279
+ @property
280
+ def fields(self):
281
+ """
282
+ Returns a list of valid fields for this value
283
+ """
284
+ return (None, "value", "signature")
285
+
286
+ def copy(self):
287
+ """
288
+ Returns a copy of this value.
289
+ """
290
+
291
+ return copy.deepcopy(self)
292
+
293
+ def _set_type(self, sctype):
294
+ self.__type = NodeType.parse(sctype)
295
+
296
+ def __compute_signature(self, person, key, salt):
297
+ h = blake2b(key=key, salt=salt, person=person)
298
+ for field in self.fields:
299
+ if field is None:
300
+ continue
301
+ if field == 'signature':
302
+ # dont include signature field in hash
303
+ continue
304
+ h.update(str(self.get(field=field)).encode("utf-8"))
305
+ return h.hexdigest()
306
+
307
+ def sign(self, person, key, salt=None):
308
+ """
309
+ Generate a signature for this value.
310
+
311
+ Args:
312
+ person (str): Identification for this person signing this value
313
+ key (str): Key to used to sign this value
314
+ salt (bytes): salt to use, if not specified, a random number will be selected.
315
+ """
316
+ if not _has_sign:
317
+ raise RuntimeError("encoding not available")
318
+
319
+ person = person.encode("utf-8")
320
+ key = key.encode("utf-8")
321
+ if not salt:
322
+ salt = os.urandom(blake2b.SALT_SIZE)
323
+ else:
324
+ salt = salt.encode("utf-8")
325
+
326
+ digest = self.__compute_signature(person=person, key=key, salt=salt)
327
+ encode_person = b64encode(person).decode("utf-8")
328
+ encode_salt = b64encode(salt).decode("utf-8")
329
+ self.__signature = f"{encode_person}:{encode_salt}:{digest}"
330
+
331
+ def verify_signature(self, person, key):
332
+ """
333
+ Verify the signature of this value.
334
+
335
+ Args:
336
+ person (str): Identification for this person signing this value
337
+ key (str): Key to used to sign this value
338
+ """
339
+ if not self.__signature:
340
+ raise ValueError("no signature available")
341
+
342
+ if not _has_sign:
343
+ raise RuntimeError("encoding not available")
344
+
345
+ key = key.encode("utf-8")
346
+ person = person.encode("utf-8")
347
+ encode_person, encode_salt, digest = self.__signature.split(":")
348
+ check_person = b64encode(person).decode("utf-8")
349
+
350
+ if check_person != encode_person:
351
+ raise ValueError(f"{person.decode('utf-8')} does not match signing "
352
+ f"person: {b64decode(encode_person).decode('utf-8')}")
353
+
354
+ decode_salt = b64decode(encode_salt)
355
+ check_digest = self.__compute_signature(person=person, key=key, salt=decode_salt)
356
+
357
+ if check_digest == digest:
358
+ return True
359
+ return False
360
+
361
+ @property
362
+ def type(self):
363
+ """
364
+ Returns the type for this value
365
+ """
366
+ return self.__type
367
+
368
+
369
+ class PathNodeValue(NodeValue):
370
+ '''
371
+ Holds the path data for a parameter.
372
+
373
+ Args:
374
+ type (str): type of path
375
+ value (any): default value for this parameter
376
+ '''
377
+
378
+ def __init__(self, type, value=None):
379
+ super().__init__(type, value=value)
380
+ self.__filehash = None
381
+ self.__package = None
382
+
383
+ def getdict(self):
384
+ return {
385
+ **super().getdict(),
386
+ "filehash": self.get(field="filehash"),
387
+ "package": self.get(field="package")
388
+ }
389
+
390
+ def _from_dict(self, manifest, keypath, version):
391
+ super()._from_dict(manifest, keypath, version)
392
+
393
+ self.set(manifest["filehash"], field="filehash")
394
+ self.set(manifest["package"], field="package")
395
+
396
+ def get(self, field='value'):
397
+ if field == 'filehash':
398
+ return self.__filehash
399
+ if field == 'package':
400
+ return self.__package
401
+ return super().get(field=field)
402
+
403
+ def set(self, value, field='value'):
404
+ if field == 'filehash':
405
+ self.__filehash = NodeType.normalize(value, "str")
406
+ return self
407
+ if field == 'package':
408
+ self.__package = NodeType.normalize(value, "str")
409
+ return self
410
+ return super().set(value, field=field)
411
+
412
+ def __resolve_collection_path(self, path, collection_dir):
413
+ try:
414
+ collected_paths = os.listdir(collection_dir)
415
+ if not collected_paths:
416
+ return None
417
+ except FileNotFoundError:
418
+ return None
419
+
420
+ path_paths = pathlib.PurePosixPath(path).parts
421
+ for n in range(len(path_paths)):
422
+ # Search through the path elements to see if any of the previous path parts
423
+ # have been imported
424
+
425
+ n += 1
426
+ basename = str(pathlib.PurePosixPath(*path_paths[0:n]))
427
+ endname = str(pathlib.PurePosixPath(*path_paths[n:]))
428
+
429
+ import_name = PathNodeValue.generate_hashed_path(basename, self.__package)
430
+ if import_name not in collected_paths:
431
+ continue
432
+
433
+ abspath = os.path.join(collection_dir, import_name)
434
+ if endname:
435
+ abspath = os.path.join(abspath, endname)
436
+ abspath = os.path.abspath(abspath)
437
+ if os.path.exists(abspath):
438
+ return abspath
439
+
440
+ return None
441
+
442
+ @staticmethod
443
+ def resolve_env_vars(path, envvars=None):
444
+ """
445
+ Resolve environment variables in a path.
446
+
447
+ Returns the expended path.
448
+
449
+ Args:
450
+ path (str): path to expand
451
+ envvars (dict): environmental variables to use during resolution.
452
+ """
453
+
454
+ if not path:
455
+ return path
456
+
457
+ # Handle environmental expansions
458
+ if not envvars:
459
+ envvars = {}
460
+
461
+ # Resolve env vars and user home
462
+ env_save = os.environ.copy()
463
+ os.environ.update(envvars)
464
+ path = os.path.expandvars(path)
465
+ path = os.path.expanduser(path)
466
+ os.environ.clear()
467
+ os.environ.update(env_save)
468
+
469
+ return path
470
+
471
+ def resolve_path(self, envvars=None, search=None, collection_dir=None):
472
+ """
473
+ Resolve the path of this value.
474
+
475
+ Returns the absolute path if found, otherwise raises a FileNotFoundError.
476
+
477
+ Args:
478
+ envvars (dict): environmental variables to use during resolution.
479
+ search (list of paths): list of paths to search to check for the path.
480
+ collection_dir (path): path to collection directory.
481
+ """
482
+ value = self.get()
483
+ if value is None:
484
+ return None
485
+
486
+ # Handle environmental expansions
487
+ value = PathNodeValue.resolve_env_vars(value, envvars=envvars)
488
+
489
+ # Check collections path
490
+ if collection_dir:
491
+ collect_path = self.__resolve_collection_path(value, collection_dir)
492
+ if collect_path:
493
+ return collect_path
494
+
495
+ if os.path.isabs(value) and os.path.exists(value):
496
+ return value
497
+
498
+ # Search for file
499
+ if search is None:
500
+ search = [os.getcwd()]
501
+
502
+ for searchdir in search:
503
+ abspath = os.path.abspath(os.path.join(searchdir, value))
504
+ if os.path.exists(abspath):
505
+ return abspath
506
+
507
+ # File not found
508
+ raise FileNotFoundError(value)
509
+
510
+ @staticmethod
511
+ def generate_hashed_path(path, package):
512
+ '''
513
+ Utility to map file to an unambiguous name based on its path.
514
+
515
+ The mapping looks like:
516
+ path/to/file.ext => file_<hash('path/to')>.ext
517
+
518
+ Args:
519
+ path (str): path to directory or file
520
+ package (str): name of package this file belongs to
521
+ '''
522
+ path = pathlib.PurePosixPath(path)
523
+ ext = ''.join(path.suffixes)
524
+
525
+ # strip off all file suffixes to get just the bare name
526
+ barepath = path
527
+ while barepath.suffix:
528
+ barepath = pathlib.PurePosixPath(barepath.stem)
529
+ filename = str(barepath.parts[-1])
530
+
531
+ if not package:
532
+ package = ''
533
+ else:
534
+ package = f'{package}:'
535
+
536
+ path_to_hash = f'{package}{str(path.parent)}'
537
+
538
+ pathhash = hashlib.sha1(path_to_hash.encode('utf-8')).hexdigest()
539
+
540
+ return f'{filename}_{pathhash}{ext}'
541
+
542
+ def get_hashed_filename(self):
543
+ '''
544
+ Utility to map file to an unambiguous name based on its path.
545
+
546
+ The mapping looks like:
547
+ path/to/file.ext => file_<hash('path/to')>.ext
548
+ '''
549
+ return PathNodeValue.generate_hashed_path(self.get(), self.__package)
550
+
551
+ def hash(self, function, **kwargs):
552
+ """
553
+ Compute the hash for this path.
554
+
555
+ Keyword arguments are derived from :meth:`resolve_path`.
556
+
557
+ Args:
558
+ function (str): name of hashing function to use.
559
+ """
560
+ raise NotImplementedError
561
+
562
+ @staticmethod
563
+ def hash_directory(dirname, hashobj=None, hashfunction=None):
564
+ """
565
+ Compute the hash for this directory.
566
+
567
+ Args:
568
+ dirname (path): directory to hash
569
+ hashobj (hashlib.): hashing object
570
+ hashfunction (str): name of hashing function to use
571
+ """
572
+
573
+ if dirname is None:
574
+ return None
575
+
576
+ if not hashobj:
577
+ hashfunc = getattr(hashlib, hashfunction, None)
578
+ if not hashfunc:
579
+ raise RuntimeError("Unable to hash directory due to missing "
580
+ f"hash function: {hashfunction}")
581
+ hashobj = hashfunc()
582
+
583
+ all_files = []
584
+ for root, _, files in os.walk(dirname):
585
+ all_files.extend([os.path.join(root, f) for f in files])
586
+ dirhash = None
587
+ hashobj = hashfunc()
588
+ for file in sorted(all_files):
589
+ # Cast everything to a windows path and convert to posix.
590
+ # https://stackoverflow.com/questions/73682260
591
+ posix_path = pathlib.PureWindowsPath(os.path.relpath(file, dirname)).as_posix()
592
+ hashobj.update(posix_path.encode("utf-8"))
593
+ dirhash = PathNodeValue.hash_file(file, hashobj=hashobj)
594
+ return dirhash
595
+
596
+ @staticmethod
597
+ def hash_file(filename, hashobj=None, hashfunction=None):
598
+ """
599
+ Compute the hash for this file.
600
+
601
+ Args:
602
+ filename (path): file to hash
603
+ hashobj (hashlib.): hashing object
604
+ hashfunction (str): name of hashing function to use
605
+ """
606
+
607
+ if filename is None:
608
+ return None
609
+
610
+ if not hashobj:
611
+ hashfunc = getattr(hashlib, hashfunction, None)
612
+ if not hashfunc:
613
+ raise RuntimeError("Unable to hash file due to missing "
614
+ f"hash function: {hashfunction}")
615
+ hashobj = hashfunc()
616
+
617
+ with open(filename, "rb") as f:
618
+ for byte_block in iter(lambda: f.read(4096), b""):
619
+ hashobj.update(byte_block)
620
+ return hashobj.hexdigest()
621
+
622
+ @property
623
+ def fields(self):
624
+ return (*super().fields, "filehash", "package")
625
+
626
+ @property
627
+ def type(self):
628
+ raise NotImplementedError
629
+
630
+
631
+ class DirectoryNodeValue(PathNodeValue):
632
+ '''
633
+ Holds the directory data for a parameter.
634
+
635
+ Args:
636
+ value (any): default value for this parameter
637
+ '''
638
+
639
+ def __init__(self, value=None):
640
+ super().__init__("dir", value=value)
641
+
642
+ def hash(self, function, **kwargs):
643
+ """
644
+ Compute the hash for this directory.
645
+
646
+ Keyword arguments are derived from :meth:`resolve_path`.
647
+
648
+ Args:
649
+ function (str): name of hashing function to use.
650
+ """
651
+ return PathNodeValue.hash_directory(
652
+ self.resolve_path(**kwargs), hashfunction=function)
653
+
654
+ @property
655
+ def type(self):
656
+ return "dir"
657
+
658
+
659
+ class FileNodeValue(PathNodeValue):
660
+ '''
661
+ Holds the file data for a parameter.
662
+
663
+ Args:
664
+ value (any): default value for this parameter
665
+ '''
666
+
667
+ def __init__(self, value=None):
668
+ super().__init__("file", value=value)
669
+ self.__date = None
670
+ self.__author = []
671
+
672
+ def getdict(self):
673
+ return {
674
+ **super().getdict(),
675
+ "date": self.get(field="date"),
676
+ "author": self.get(field="author")
677
+ }
678
+
679
+ def _from_dict(self, manifest, keypath, version):
680
+ super()._from_dict(manifest, keypath, version)
681
+
682
+ self.set(manifest["date"], field="date")
683
+ self.set(manifest["author"], field="author")
684
+
685
+ def get(self, field='value'):
686
+ if field == 'date':
687
+ return self.__date
688
+ if field == 'author':
689
+ return self.__author.copy()
690
+ return super().get(field=field)
691
+
692
+ def set(self, value, field='value'):
693
+ if field == 'date':
694
+ self.__date = NodeType.normalize(value, "str")
695
+ return self
696
+ if field == 'author':
697
+ self.__author = NodeType.normalize(value, ["str"])
698
+ return self
699
+ return super().set(value, field=field)
700
+
701
+ def add(self, value, field='value'):
702
+ """
703
+ Adds the value in a specific field and ensures it has been normalized.
704
+
705
+ Returns:
706
+ self
707
+
708
+ Args:
709
+ value (any): value to set
710
+ field (str): field to set
711
+ """
712
+
713
+ if field == 'author':
714
+ self.__author.extend(NodeType.normalize(value, ["str"]))
715
+ return self
716
+ return super().add(value, field=field)
717
+
718
+ def hash(self, function, **kwargs):
719
+ """
720
+ Compute the hash for this file.
721
+
722
+ Keyword arguments are derived from :meth:`resolve_path`.
723
+
724
+ Args:
725
+ function (str): name of hashing function to use.
726
+ """
727
+ return PathNodeValue.hash_file(
728
+ self.resolve_path(**kwargs), hashfunction=function)
729
+
730
+ @property
731
+ def fields(self):
732
+ return (*super().fields, "date", "author")
733
+
734
+ @property
735
+ def type(self):
736
+ return "file"