librelane 2.4.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.

Potentially problematic release.


This version of librelane might be problematic. Click here for more details.

Files changed (170) hide show
  1. librelane/__init__.py +38 -0
  2. librelane/__main__.py +479 -0
  3. librelane/__version__.py +43 -0
  4. librelane/common/__init__.py +63 -0
  5. librelane/common/cli.py +75 -0
  6. librelane/common/drc.py +246 -0
  7. librelane/common/generic_dict.py +319 -0
  8. librelane/common/metrics/__init__.py +35 -0
  9. librelane/common/metrics/__main__.py +413 -0
  10. librelane/common/metrics/library.py +354 -0
  11. librelane/common/metrics/metric.py +186 -0
  12. librelane/common/metrics/util.py +279 -0
  13. librelane/common/misc.py +456 -0
  14. librelane/common/ring_buffer.py +63 -0
  15. librelane/common/tcl.py +80 -0
  16. librelane/common/toolbox.py +549 -0
  17. librelane/common/tpe.py +41 -0
  18. librelane/common/types.py +116 -0
  19. librelane/config/__init__.py +32 -0
  20. librelane/config/__main__.py +155 -0
  21. librelane/config/config.py +1025 -0
  22. librelane/config/flow.py +490 -0
  23. librelane/config/pdk_compat.py +255 -0
  24. librelane/config/preprocessor.py +464 -0
  25. librelane/config/removals.py +45 -0
  26. librelane/config/variable.py +743 -0
  27. librelane/container.py +285 -0
  28. librelane/env_info.py +320 -0
  29. librelane/examples/spm/config.yaml +33 -0
  30. librelane/examples/spm/pin_order.cfg +14 -0
  31. librelane/examples/spm/src/impl.sdc +73 -0
  32. librelane/examples/spm/src/signoff.sdc +68 -0
  33. librelane/examples/spm/src/spm.v +73 -0
  34. librelane/examples/spm/verify/spm_tb.v +106 -0
  35. librelane/examples/spm-user_project_wrapper/SPM_example.v +286 -0
  36. librelane/examples/spm-user_project_wrapper/base_sdc_file.sdc +145 -0
  37. librelane/examples/spm-user_project_wrapper/config-tut.json +12 -0
  38. librelane/examples/spm-user_project_wrapper/config.json +13 -0
  39. librelane/examples/spm-user_project_wrapper/defines.v +66 -0
  40. librelane/examples/spm-user_project_wrapper/template.def +7656 -0
  41. librelane/examples/spm-user_project_wrapper/user_project_wrapper.v +123 -0
  42. librelane/flows/__init__.py +24 -0
  43. librelane/flows/builtins.py +18 -0
  44. librelane/flows/classic.py +327 -0
  45. librelane/flows/cli.py +463 -0
  46. librelane/flows/flow.py +1049 -0
  47. librelane/flows/misc.py +71 -0
  48. librelane/flows/optimizing.py +179 -0
  49. librelane/flows/sequential.py +367 -0
  50. librelane/flows/synth_explore.py +173 -0
  51. librelane/help/__main__.py +39 -0
  52. librelane/logging/__init__.py +40 -0
  53. librelane/logging/logger.py +323 -0
  54. librelane/open_pdks_rev +1 -0
  55. librelane/plugins.py +21 -0
  56. librelane/py.typed +0 -0
  57. librelane/scripts/base.sdc +80 -0
  58. librelane/scripts/klayout/Readme.md +2 -0
  59. librelane/scripts/klayout/open_design.py +63 -0
  60. librelane/scripts/klayout/render.py +121 -0
  61. librelane/scripts/klayout/stream_out.py +176 -0
  62. librelane/scripts/klayout/xml_drc_report_to_json.py +45 -0
  63. librelane/scripts/klayout/xor.drc +120 -0
  64. librelane/scripts/magic/Readme.md +1 -0
  65. librelane/scripts/magic/common/read.tcl +114 -0
  66. librelane/scripts/magic/def/antenna_check.tcl +35 -0
  67. librelane/scripts/magic/def/mag.tcl +19 -0
  68. librelane/scripts/magic/def/mag_gds.tcl +79 -0
  69. librelane/scripts/magic/drc.tcl +78 -0
  70. librelane/scripts/magic/extract_spice.tcl +98 -0
  71. librelane/scripts/magic/gds/drc_batch.tcl +74 -0
  72. librelane/scripts/magic/gds/erase_box.tcl +32 -0
  73. librelane/scripts/magic/gds/extras_mag.tcl +45 -0
  74. librelane/scripts/magic/gds/mag_with_pointers.tcl +31 -0
  75. librelane/scripts/magic/get_bbox.tcl +11 -0
  76. librelane/scripts/magic/lef/extras_maglef.tcl +61 -0
  77. librelane/scripts/magic/lef/maglef.tcl +26 -0
  78. librelane/scripts/magic/lef.tcl +57 -0
  79. librelane/scripts/magic/open.tcl +28 -0
  80. librelane/scripts/magic/wrapper.tcl +21 -0
  81. librelane/scripts/netgen/setup.tcl +28 -0
  82. librelane/scripts/odbpy/apply_def_template.py +49 -0
  83. librelane/scripts/odbpy/cell_frequency.py +107 -0
  84. librelane/scripts/odbpy/check_antenna_properties.py +116 -0
  85. librelane/scripts/odbpy/contextualize.py +109 -0
  86. librelane/scripts/odbpy/defutil.py +573 -0
  87. librelane/scripts/odbpy/diodes.py +373 -0
  88. librelane/scripts/odbpy/disconnected_pins.py +305 -0
  89. librelane/scripts/odbpy/eco_buffer.py +181 -0
  90. librelane/scripts/odbpy/eco_diode.py +139 -0
  91. librelane/scripts/odbpy/filter_unannotated.py +100 -0
  92. librelane/scripts/odbpy/io_place.py +482 -0
  93. librelane/scripts/odbpy/ioplace_parser/__init__.py +23 -0
  94. librelane/scripts/odbpy/ioplace_parser/parse.py +147 -0
  95. librelane/scripts/odbpy/label_macro_pins.py +277 -0
  96. librelane/scripts/odbpy/lefutil.py +97 -0
  97. librelane/scripts/odbpy/placers.py +162 -0
  98. librelane/scripts/odbpy/power_utils.py +397 -0
  99. librelane/scripts/odbpy/random_place.py +57 -0
  100. librelane/scripts/odbpy/reader.py +250 -0
  101. librelane/scripts/odbpy/remove_buffers.py +173 -0
  102. librelane/scripts/odbpy/snap_to_grid.py +57 -0
  103. librelane/scripts/odbpy/wire_lengths.py +93 -0
  104. librelane/scripts/openroad/antenna_check.tcl +20 -0
  105. librelane/scripts/openroad/antenna_repair.tcl +31 -0
  106. librelane/scripts/openroad/basic_mp.tcl +24 -0
  107. librelane/scripts/openroad/buffer_list.tcl +10 -0
  108. librelane/scripts/openroad/common/dpl.tcl +24 -0
  109. librelane/scripts/openroad/common/dpl_cell_pad.tcl +26 -0
  110. librelane/scripts/openroad/common/grt.tcl +32 -0
  111. librelane/scripts/openroad/common/io.tcl +540 -0
  112. librelane/scripts/openroad/common/pdn_cfg.tcl +135 -0
  113. librelane/scripts/openroad/common/resizer.tcl +103 -0
  114. librelane/scripts/openroad/common/set_global_connections.tcl +78 -0
  115. librelane/scripts/openroad/common/set_layer_adjustments.tcl +31 -0
  116. librelane/scripts/openroad/common/set_power_nets.tcl +30 -0
  117. librelane/scripts/openroad/common/set_rc.tcl +75 -0
  118. librelane/scripts/openroad/common/set_routing_layers.tcl +30 -0
  119. librelane/scripts/openroad/cts.tcl +80 -0
  120. librelane/scripts/openroad/cut_rows.tcl +24 -0
  121. librelane/scripts/openroad/dpl.tcl +24 -0
  122. librelane/scripts/openroad/drt.tcl +37 -0
  123. librelane/scripts/openroad/fill.tcl +30 -0
  124. librelane/scripts/openroad/floorplan.tcl +145 -0
  125. librelane/scripts/openroad/gpl.tcl +88 -0
  126. librelane/scripts/openroad/grt.tcl +30 -0
  127. librelane/scripts/openroad/gui.tcl +37 -0
  128. librelane/scripts/openroad/insert_buffer.tcl +127 -0
  129. librelane/scripts/openroad/ioplacer.tcl +67 -0
  130. librelane/scripts/openroad/irdrop.tcl +51 -0
  131. librelane/scripts/openroad/pdn.tcl +52 -0
  132. librelane/scripts/openroad/rcx.tcl +32 -0
  133. librelane/scripts/openroad/repair_design.tcl +70 -0
  134. librelane/scripts/openroad/repair_design_postgrt.tcl +48 -0
  135. librelane/scripts/openroad/rsz_timing_postcts.tcl +68 -0
  136. librelane/scripts/openroad/rsz_timing_postgrt.tcl +70 -0
  137. librelane/scripts/openroad/sta/check_macro_instances.tcl +53 -0
  138. librelane/scripts/openroad/sta/corner.tcl +393 -0
  139. librelane/scripts/openroad/tapcell.tcl +25 -0
  140. librelane/scripts/openroad/write_views.tcl +27 -0
  141. librelane/scripts/pyosys/construct_abc_script.py +177 -0
  142. librelane/scripts/pyosys/json_header.py +84 -0
  143. librelane/scripts/pyosys/synthesize.py +493 -0
  144. librelane/scripts/pyosys/ys_common.py +153 -0
  145. librelane/scripts/tclsh/hello.tcl +1 -0
  146. librelane/state/__init__.py +24 -0
  147. librelane/state/__main__.py +61 -0
  148. librelane/state/design_format.py +195 -0
  149. librelane/state/state.py +359 -0
  150. librelane/steps/__init__.py +61 -0
  151. librelane/steps/__main__.py +510 -0
  152. librelane/steps/checker.py +637 -0
  153. librelane/steps/common_variables.py +340 -0
  154. librelane/steps/cvc_rv.py +169 -0
  155. librelane/steps/klayout.py +509 -0
  156. librelane/steps/magic.py +576 -0
  157. librelane/steps/misc.py +160 -0
  158. librelane/steps/netgen.py +253 -0
  159. librelane/steps/odb.py +1088 -0
  160. librelane/steps/openroad.py +2460 -0
  161. librelane/steps/openroad_alerts.py +102 -0
  162. librelane/steps/pyosys.py +640 -0
  163. librelane/steps/step.py +1571 -0
  164. librelane/steps/tclstep.py +288 -0
  165. librelane/steps/verilator.py +222 -0
  166. librelane/steps/yosys.py +371 -0
  167. librelane-2.4.0.dist-info/METADATA +169 -0
  168. librelane-2.4.0.dist-info/RECORD +170 -0
  169. librelane-2.4.0.dist-info/WHEEL +4 -0
  170. librelane-2.4.0.dist-info/entry_points.txt +9 -0
@@ -0,0 +1,456 @@
1
+ # Copyright 2023 Efabless Corporation
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ import io
15
+ import os
16
+ import re
17
+ import glob
18
+ import gzip
19
+ import shutil
20
+ import typing
21
+ import fnmatch
22
+ import pathlib
23
+ import unicodedata
24
+ from math import inf
25
+ from typing import (
26
+ IO,
27
+ Any,
28
+ Generator,
29
+ Iterable,
30
+ List,
31
+ TypeVar,
32
+ Optional,
33
+ SupportsFloat,
34
+ Union,
35
+ )
36
+
37
+ import httpx
38
+
39
+ from .types import AnyPath, Path
40
+ from ..__version__ import __version__
41
+
42
+ T = TypeVar("T")
43
+
44
+
45
+ def idem(obj: T, *args, **kwargs) -> T:
46
+ """
47
+ :returns: the parameter ``obj`` unchanged. Useful for some lambdas.
48
+ """
49
+ return obj
50
+
51
+
52
+ def get_librelane_root() -> str:
53
+ """
54
+ Returns the root LibreLane folder, i.e., the folder containing the
55
+ ``__init__.py``.
56
+ """
57
+ return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
58
+
59
+
60
+ def get_script_dir() -> str:
61
+ """
62
+ Gets the LibreLane tool `scripts` directory.
63
+
64
+ :meta private:
65
+ """
66
+ return os.path.join(
67
+ get_librelane_root(),
68
+ "scripts",
69
+ )
70
+
71
+
72
+ def get_opdks_rev() -> str:
73
+ """
74
+ Gets the Open_PDKs revision confirmed compatible with this version of LibreLane.
75
+ """
76
+ return (
77
+ open(os.path.join(get_librelane_root(), "open_pdks_rev"), encoding="utf8")
78
+ .read()
79
+ .strip()
80
+ )
81
+
82
+
83
+ # The following code snippet has been adapted under the following license:
84
+ #
85
+ # Copyright (c) Django Software Foundation and individual contributors.
86
+ # All rights reserved.
87
+
88
+ # Redistribution and use in source and binary forms, with or without modification,
89
+ # are permitted provided that the following conditions are met:
90
+
91
+ # 1. Redistributions of source code must retain the above copyright notice,
92
+ # this list of conditions and the following disclaimer.
93
+
94
+ # 2. Redistributions in binary form must reproduce the above copyright
95
+ # notice, this list of conditions and the following disclaimer in the
96
+ # documentation and/or other materials provided with the distribution.
97
+
98
+ # 3. Neither the name of Django nor the names of its contributors may be used
99
+ # to endorse or promote products derived from this software without
100
+ # specific prior written permission.
101
+
102
+
103
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
104
+ # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
105
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
106
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
107
+ # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
108
+ # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
109
+ # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
110
+ # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
111
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
112
+ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
113
+ def slugify(value: str, lower: bool = False) -> str:
114
+ """
115
+ :param value: Input string
116
+ :returns: The input string converted to lower case, with all characters
117
+ except alphanumerics, underscores and hyphens removed, and spaces and\
118
+ dots converted into hyphens.
119
+
120
+ Leading and trailing whitespace is stripped.
121
+ """
122
+ if lower:
123
+ value = value.lower()
124
+ value = (
125
+ unicodedata.normalize("NFKD", value).encode("ascii", "ignore").decode("ascii")
126
+ )
127
+ value = re.sub(r"[^\w\s\-\.]", "", value).strip().lower()
128
+ return re.sub(r"[\s\.]+", "-", value)
129
+
130
+
131
+ def protected(method):
132
+ """A decorator to indicate protected methods.
133
+
134
+ It dynamically adds a statement to the effect in the docstring as well
135
+ as setting an attribute, ``protected``, to ``True``, but has no other effects.
136
+
137
+ :param f: Method to mark as protected
138
+ """
139
+ if method.__doc__ is None:
140
+ method.__doc__ = ""
141
+ method.__doc__ = "**protected**\n" + method.__doc__
142
+
143
+ setattr(method, "protected", True)
144
+ return method
145
+
146
+
147
+ final = typing.final
148
+ final.__doc__ = """A decorator to indicate final methods and final classes.
149
+
150
+ Use this decorator to indicate to type checkers that the decorated
151
+ method cannot be overridden, and decorated class cannot be subclassed.
152
+ For example:
153
+
154
+
155
+ .. code-block:: python
156
+
157
+ class Base:
158
+ @final
159
+ def done(self) -> None:
160
+ ...
161
+ class Sub(Base):
162
+ def done(self) -> None: # Error reported by type checker
163
+ ...
164
+
165
+ @final
166
+ class Leaf:
167
+ ...
168
+ class Other(Leaf): # Error reported by type checker
169
+ ...
170
+
171
+ There is no runtime checking of these properties.
172
+ """
173
+
174
+
175
+ def mkdirp(path: typing.Union[str, os.PathLike]):
176
+ """
177
+ Attempts to create a directory and all of its parents.
178
+
179
+ Does not fail if the directory already exists, however, it does fail
180
+ if it is unable to create any of the components and/or if the path
181
+ already exists as a file.
182
+
183
+ :param path: A filesystem path for the directory
184
+ """
185
+ return pathlib.Path(path).mkdir(parents=True, exist_ok=True)
186
+
187
+
188
+ class zip_first(object):
189
+ """
190
+ Works like ``zip_longest`` if |a| > |b| and ``zip`` if |a| <= |b|.
191
+ """
192
+
193
+ def __init__(self, a: Iterable, b: Iterable, fillvalue: Any) -> None:
194
+ self.a = a
195
+ self.b = b
196
+ self.fillvalue = fillvalue
197
+
198
+ def __iter__(self):
199
+ self.iter_a = iter(self.a)
200
+ self.iter_b = iter(self.b)
201
+ return self
202
+
203
+ def __next__(self):
204
+ a = next(self.iter_a)
205
+ b = self.fillvalue
206
+ try:
207
+ b = next(self.iter_b)
208
+ except StopIteration:
209
+ pass
210
+ return (a, b)
211
+
212
+
213
+ def format_size(byte_count: int) -> str:
214
+ units = [
215
+ "B",
216
+ "KiB",
217
+ "MiB",
218
+ "GiB",
219
+ "TiB",
220
+ "PiB",
221
+ "EiB",
222
+ # TODO: update LibreLane when zebibytes are a thing
223
+ ]
224
+
225
+ tracker = 0
226
+ so_far = byte_count
227
+ while (so_far // 1024) > 0 and tracker < (len(units) - 1):
228
+ tracker += 1
229
+ so_far //= 1024
230
+
231
+ return f"{so_far}{units[tracker]}"
232
+
233
+
234
+ def format_elapsed_time(elapsed_seconds: SupportsFloat) -> str:
235
+ """
236
+ :param elapsed_seconds: Total time elapsed in seconds
237
+ :returns: A string in the format ``{hours}:{minutes}:{seconds}:{milliseconds}``
238
+ """
239
+ elapsed_seconds = float(elapsed_seconds)
240
+
241
+ hours = int(elapsed_seconds // 3600)
242
+ leftover = elapsed_seconds % 3600
243
+
244
+ minutes = int(leftover // 60)
245
+ leftover = leftover % 60
246
+
247
+ seconds = int(leftover // 1)
248
+ milliseconds = int((leftover % 1) * 1000)
249
+
250
+ return f"{hours:02}:{minutes:02}:{seconds:02}.{milliseconds:03}"
251
+
252
+
253
+ class Filter(object):
254
+ """
255
+ Encapsulates commonly used wildcard-based filtering functions into an object.
256
+
257
+ :param filters: A list of a wildcards supporting the
258
+ `fnmatch spec <https://docs.python.org/3.10/library/fnmatch.html>`_.
259
+
260
+ The wildcards will be split into an "allow" and "deny" list based on whether
261
+ the filter is prefixed with a ``!``.
262
+ """
263
+
264
+ def __init__(self, filters: Iterable[str]):
265
+ self.allow = []
266
+ self.deny = []
267
+ for filter in filters:
268
+ if filter.startswith("!"):
269
+ self.deny.append(filter[1:])
270
+ else:
271
+ self.allow.append(filter)
272
+
273
+ def get_matching_wildcards(self, input: str) -> Generator[str, Any, None]:
274
+ """
275
+ :param input: An input to match wildcards against.
276
+ :returns: An iterable object for *all* wildcards in the allow list
277
+ accepting ``input``, and *all* wildcards in the deny list rejecting
278
+ ``input``.
279
+ """
280
+ for wildcard in self.allow:
281
+ if fnmatch.fnmatch(input, wildcard):
282
+ yield wildcard
283
+ for wildcard in self.deny:
284
+ if not fnmatch.fnmatch(input, wildcard):
285
+ yield wildcard
286
+
287
+ def match(self, input: str) -> bool:
288
+ """
289
+ :param input: An input string to either accept or reject
290
+ :returns: A boolean indicating whether the input:
291
+ * Has matched at least one wildcard in the allow list
292
+ * Has matched exactly 0 inputs in the deny list
293
+ """
294
+ allowed = False
295
+ for wildcard in self.allow:
296
+ if fnmatch.fnmatch(input, wildcard):
297
+ allowed = True
298
+ break
299
+ for wildcard in self.deny:
300
+ if fnmatch.fnmatch(input, wildcard):
301
+ allowed = False
302
+ break
303
+ return allowed
304
+
305
+ def filter(
306
+ self,
307
+ inputs: Iterable[str],
308
+ ) -> Generator[str, Any, None]:
309
+ """
310
+ :param inputs: A series of inputs to filter according to the wildcards.
311
+ :returns: An iterable object of any values in ``inputs`` that:
312
+ * Have matched at least one wildcard in the allow list
313
+ * Have matched exactly 0 inputs in the deny list
314
+ """
315
+ for input in inputs:
316
+ if self.match(input):
317
+ yield input
318
+
319
+
320
+ def recreate_tree(
321
+ source: AnyPath,
322
+ target: AnyPath,
323
+ ):
324
+ """
325
+ This function attempts to recreate a file tree from a source path in another
326
+ target path.
327
+
328
+ Permissions are not copied over. Symlinks and hardlinks are followed.
329
+
330
+ Directories are not recreated unless they contain files as (grand)children.
331
+
332
+ If the source and target are the same, the function returns early and does
333
+ nothing.
334
+
335
+ :param source: The source file tree to replicate
336
+ :param target: The target path to recreate the file tree within
337
+ """
338
+ source = os.path.abspath(source)
339
+ target = os.path.abspath(target)
340
+ if os.path.exists(target) and os.path.samefile(source, target):
341
+ return
342
+ for dirname, _, files in os.walk(source):
343
+ for file in files:
344
+ resolved = os.path.join(dirname, file)
345
+ resolved_target = os.path.join(target, os.path.relpath(resolved, source))
346
+ os.makedirs(os.path.dirname(resolved_target), exist_ok=True)
347
+ with open(resolved, "rb") as fi, open(resolved_target, "wb") as fo:
348
+ shutil.copyfileobj(fi, fo)
349
+
350
+
351
+ def get_latest_file(in_path: Union[str, os.PathLike], filename: str) -> Optional[Path]:
352
+ """
353
+ :param in_path: A directory to search in
354
+ :param filename: The final filename
355
+ :returns: The latest file matching the parameters, by modification time
356
+ """
357
+ glob_results = glob.glob(os.path.join(in_path, "**", filename), recursive=True)
358
+ latest_time = -inf
359
+ latest_json = None
360
+ for result in glob_results:
361
+ time = os.path.getmtime(result)
362
+ if time > latest_time:
363
+ latest_time = time
364
+ latest_json = Path(result)
365
+
366
+ return latest_json
367
+
368
+
369
+ def get_httpx_session(token: Optional[str] = None) -> httpx.Client:
370
+ """
371
+ Creates an ``httpx`` session client that follows redirects and has the
372
+ User-Agent header set to ``librelane/{__version__}``.
373
+
374
+ :param token: If this parameter is non-None and not empty, another header,
375
+ Authorization: Bearer {token}, is included.
376
+ :returns: The created client
377
+ """
378
+ session = httpx.Client(follow_redirects=True)
379
+ headers_raw = {"User-Agent": f"librelane/{__version__}"}
380
+ if token is not None and token.strip() != "":
381
+ headers_raw["Authorization"] = f"Bearer {token}"
382
+ session.headers = httpx.Headers(headers_raw)
383
+ return session
384
+
385
+
386
+ def process_list_file(from_file: AnyPath) -> List[str]:
387
+ """
388
+ Convenience function to process text files in a ``.gitignore``-style format,
389
+ i.e., those where the lines may be:
390
+
391
+ * A list element
392
+ * A comment prefixed with ``#``
393
+ * Blank
394
+
395
+ :param from_file: The input text file.
396
+ :returns: A list of the strings listed in the file, ignoring lines
397
+ prefixed with a ``#`` and empty lines.
398
+ """
399
+ excluded_cells = []
400
+ list_str = open(str(from_file), encoding="utf8").read()
401
+ for line in list_str.splitlines():
402
+ line = line.strip()
403
+ if line == "":
404
+ continue
405
+ if line[0] == "#":
406
+ continue
407
+ excluded_cells.append(line)
408
+ return excluded_cells
409
+
410
+
411
+ def _get_process_limit() -> int:
412
+ return int(os.getenv("_OPENLANE_MAX_CORES", os.cpu_count() or 1))
413
+
414
+
415
+ def gzopen(filename: AnyPath, mode="rt") -> IO[Any]:
416
+ """
417
+ This function (tries to?) emulate the gzopen from the Linux Standard Base,
418
+ specifically this part:
419
+
420
+ If path refers to an uncompressed file, and mode refers to a read mode,
421
+ gzopen() shall attempt to open the file and return a gzFile object suitable
422
+ for reading directly from the file without any decompression.
423
+
424
+ gzip.open does not have this behavior.
425
+
426
+ :param filename: The full path to the uncompressed or gzipped file.
427
+ :param mode: "r", "rb", "w", "wb", "x", "xb", "a" or "ab" for
428
+ binary mode, or "rt", "wt", "xt" or "at" for text mode.
429
+ :returns: An I/O wrapper that may very slightly based on the mode.
430
+ """
431
+ try:
432
+ g = gzip.open(filename, mode=mode)
433
+ # Incredibly, it won't actually try to figure out if it's a gzipped
434
+ # file until you try to read from it.
435
+ if "r" in mode:
436
+ g.read(1)
437
+ g.seek(0)
438
+ return g
439
+ except gzip.BadGzipFile:
440
+ g.close()
441
+ return open(filename, mode=mode)
442
+
443
+
444
+ def count_occurences(fp: io.TextIOWrapper, pattern: str = "") -> int:
445
+ """
446
+ Counts the occurences of a certain string in a stream, line-by-line, without
447
+ necessarily loading the entire file into memory.
448
+
449
+ Equivalent to: ``grep -c 'pattern' <file>`` (but without regex support).
450
+
451
+ :param fp: the text stream
452
+ :param pattern: the substring to search for. if set to "", it will simply
453
+ count the lines in the file.
454
+ :returns: the number of matching lines
455
+ """
456
+ return sum(pattern in line for line in fp)
@@ -0,0 +1,63 @@
1
+ # Copyright 2023 Efabless Corporation
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ from typing import Iterable, Iterator, Type, TypeVar
15
+
16
+ VT = TypeVar("VT")
17
+
18
+
19
+ class RingBuffer(Iterable[VT]):
20
+ """
21
+ A generic ring (circular) buffer that automatically pops the element at the
22
+ head when full, and emplaces a new element in its place.
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ t: Type[VT],
28
+ max: int,
29
+ ) -> None:
30
+ super().__init__()
31
+ self._store = [t()] * max
32
+ self._max = max
33
+ self._head = 0
34
+ self._tail = 0
35
+ self._len = 0
36
+
37
+ def pop(self) -> VT:
38
+ if self._len == 0:
39
+ raise IndexError("pop from empty ring buffer")
40
+ element = self[0]
41
+ self._head = (self._head + 1) % self._max
42
+ self._len -= 1
43
+ return element
44
+
45
+ def push(self, element: VT):
46
+ if self._len == self._max:
47
+ self.pop()
48
+ self._store[self._tail] = element
49
+ self._tail = (self._tail + 1) % self._max
50
+ self._len += 1
51
+
52
+ def __getitem__(self, idx: int, /) -> VT:
53
+ if idx + 1 > self._len:
54
+ raise IndexError(f"{idx} is out of range")
55
+ i = (self._head + idx) % self._max
56
+ return self._store[i]
57
+
58
+ def __len__(self) -> int:
59
+ return self._len
60
+
61
+ def __iter__(self) -> Iterator[VT]:
62
+ for i in range(0, self._len):
63
+ yield self[i]
@@ -0,0 +1,80 @@
1
+ # Copyright 2023 Efabless Corporation
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ import re
15
+ import tkinter
16
+ from typing import Dict, Mapping, Any, Iterable
17
+
18
+ _env_rx = re.compile(r"(?:\:\:)?env\((\w+)\)")
19
+ _find_unsafe = re.compile(r"[^\w@%+=:,./-]", re.ASCII).search
20
+ _escapes_in_quotes = re.compile(r"([\\\$\"\[])")
21
+
22
+
23
+ class TclUtils(object):
24
+ """
25
+ A collection of useful Tcl utilities.
26
+ """
27
+
28
+ def __init__(self):
29
+ raise TypeError(f"Cannot create instances of '{self.__class__.__name__}'")
30
+
31
+ @staticmethod
32
+ def escape(s: str) -> str:
33
+ """
34
+ :returns: If the string can be parsed by Tcl as a single token, the string
35
+ is returned verbatim.
36
+
37
+ Otherwise, the string is returned in double quotes, with any unsafe
38
+ characters escaped with a backslash.
39
+ """
40
+ if s == "":
41
+ return '""'
42
+ if not _find_unsafe(s):
43
+ return s
44
+ return '"' + _escapes_in_quotes.sub(r"\\\1", s).replace("\n", r"\n") + '"'
45
+
46
+ @staticmethod
47
+ def join(ss: Iterable[str]) -> str:
48
+ """
49
+ :param ss: Input list
50
+ :returns: The input list converted to a Tcl-compatible list where each
51
+ element is either a single token or double-quoted (i.e. interpreted
52
+ by Tcl as a single element.)
53
+ """
54
+ return " ".join(TclUtils.escape(arg) for arg in ss)
55
+
56
+ @staticmethod
57
+ def _eval_env(env_in: Mapping[str, Any], tcl_in: str) -> Dict[str, Any]:
58
+ interpreter = tkinter.Tcl()
59
+
60
+ interpreter.eval("array unset ::env")
61
+ for key, value in env_in.items():
62
+ interpreter.setvar(f"env({key})", str(value))
63
+
64
+ env_out = dict(env_in)
65
+
66
+ def py_set(key, value=None):
67
+ if match := _env_rx.fullmatch(key):
68
+ if value is not None:
69
+ env_out[match.group(1)] = value
70
+
71
+ py_set_name = interpreter.register(py_set)
72
+ interpreter.call("rename", py_set_name, "_py_set")
73
+ interpreter.call("rename", "set", "_orig_set")
74
+ interpreter.eval(
75
+ "proc set args { _py_set {*}$args; tailcall _orig_set {*}$args; }"
76
+ )
77
+
78
+ interpreter.eval(tcl_in)
79
+
80
+ return env_out