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,743 @@
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 shlex
15
+ import inspect
16
+ from enum import Enum
17
+ from decimal import Decimal, InvalidOperation
18
+ from dataclasses import (
19
+ _MISSING_TYPE,
20
+ MISSING,
21
+ asdict,
22
+ dataclass,
23
+ field,
24
+ fields,
25
+ is_dataclass,
26
+ )
27
+ from typing import (
28
+ ClassVar,
29
+ Dict,
30
+ List,
31
+ Literal,
32
+ Optional,
33
+ Set,
34
+ Tuple,
35
+ Union,
36
+ Mapping,
37
+ Callable,
38
+ Type,
39
+ Any,
40
+ get_origin,
41
+ get_args,
42
+ )
43
+ from ..state import DesignFormat, State
44
+ from ..common import GenericDict, Path, is_string, zip_first, Number, slugify
45
+
46
+ # Scalar = Union[Type[str], Type[Decimal], Type[Path], Type[bool]]
47
+ # VType = Union[Scalar, List[Scalar]]
48
+
49
+
50
+ class MissingRequiredVariable(ValueError):
51
+ def __init__(self, variable: "Variable", pdk: bool = False) -> None:
52
+ self.variable = variable
53
+ if self.variable.pdk:
54
+ super().__init__(
55
+ f"Required PDK variable '{self.variable.name}' did not get a specified value. This PDK may be incompatible with your flow."
56
+ )
57
+ else:
58
+ super().__init__(
59
+ f"Required variable '{self.variable.name}' did not get a specified value."
60
+ )
61
+
62
+
63
+ class Orientation(str, Enum):
64
+ N = "N"
65
+ FN = "FN"
66
+ W = "W"
67
+ FW = "FW"
68
+ S = "S"
69
+ FS = "FS"
70
+ E = "E"
71
+ FE = "FE"
72
+ # OpenAccess
73
+ R0 = "N"
74
+ MY = "FN"
75
+ R90 = "W"
76
+ MXR90 = "FW"
77
+ R180 = "S"
78
+ MX = "FS"
79
+ R270 = "E"
80
+ MYR90 = "FE"
81
+
82
+ def __str__(self) -> str:
83
+ return self.value
84
+
85
+
86
+ @dataclass
87
+ class Instance:
88
+ """
89
+ Location information for an instance of a cell or macro.
90
+
91
+ :param location: The physical co-ordinates of the object's origin. Leave
92
+ empty for automatic placement.
93
+ :param orientation: The orientation of the object's placement. 'N'/'R0' by default.
94
+ """
95
+
96
+ location: Optional[Tuple[Decimal, Decimal]]
97
+ orientation: Optional[Orientation]
98
+
99
+
100
+ @dataclass
101
+ class Macro:
102
+ """
103
+ A data structure for storing definitions of Macros.
104
+
105
+ As it is typically stored in a dictionary in its superclass, the module name
106
+ is not stored in any of these fields.
107
+
108
+ You will note most fields correspond to a :class:`librelane.state.DesignFormat`
109
+ entry IDs. This is not coincidental.
110
+
111
+ :param gds: A list of GDSII files representing the design. At least one is required.
112
+ :param lef: A list of LEF files representing the design. At least one is required.
113
+ :param instances: A dictionary of :class:`Instance` objects representing the
114
+ instances of said macro.
115
+
116
+ The keys for the dictionaries are the names of the instances.
117
+ :param nl: A list of netlists constituting the design.
118
+
119
+ The netlists must be valid Verilog netlists readable by tools such as
120
+ OpenSTA.
121
+
122
+ Can be empty, however SPEF-based hierarchical static timing analysis
123
+ will be unavailable.
124
+ :param spef: A dictionary of parasitics annotations for the various netlists of the
125
+ Macro.
126
+
127
+ The keys are wildcards for timing corners supported by a certain SPEF file.
128
+
129
+ Can be empty, however SPEF-based hierarchical static timing analysis
130
+ will be unavailable.
131
+ :param lib: A dictionary of timing library files.
132
+
133
+ The keys are wildcards for timing corners supported by a certain LIB file.
134
+
135
+ If both SPEF and LIB views are empty, the design may be black-boxed
136
+ during STA.
137
+ :param spice: A list of SPICE netlists constituting the design. May be
138
+ useful in some flows.
139
+ :param sdf: A dictionary of standard delay format files. May be useful in some flows.
140
+
141
+ The keys are wildcards for timing corners supported by a certain SPEF file.
142
+ :param json_h: A JSON file as generated by Yosys. Helpful in some flows.
143
+ """
144
+
145
+ gds: List[Path]
146
+ lef: List[Path]
147
+ instances: Dict[str, Instance] = field(default_factory=lambda: {})
148
+
149
+ vh: List[Path] = field(default_factory=lambda: [])
150
+ nl: List[Path] = field(default_factory=lambda: [])
151
+ pnl: List[Path] = field(default_factory=lambda: [])
152
+ spef: Dict[str, List[Path]] = field(default_factory=lambda: {})
153
+ lib: Dict[str, List[Path]] = field(default_factory=lambda: {})
154
+ spice: List[Path] = field(default_factory=lambda: [])
155
+ sdf: Dict[str, List[Path]] = field(default_factory=lambda: {})
156
+
157
+ json_h: Optional[Path] = None
158
+
159
+ def view_by_df(
160
+ self, df: DesignFormat
161
+ ) -> Union[None, Path, List[Path], Dict[str, List[Path]]]:
162
+ try:
163
+ return getattr(self, df.value.id)
164
+ except AttributeError:
165
+ return None
166
+
167
+ def __post_init__(self):
168
+ if len(self.gds) < 1:
169
+ raise ValueError(
170
+ "Macro definition invalid- at least one GDSII file must be specified."
171
+ )
172
+ if len(self.lef) < 1:
173
+ raise ValueError(
174
+ "Macro definition invalid- at least one LEF file must be specified."
175
+ )
176
+
177
+ def __repr__(self) -> str:
178
+ return f"{self.__class__.__qualname__}(%s)" % ", ".join(
179
+ [f"{k}={repr(v)}" for k, v in asdict(self).items()]
180
+ )
181
+
182
+ def __str__(self) -> str:
183
+ return self.__repr__()
184
+
185
+ @classmethod
186
+ def from_state(Self, state: State) -> "Macro":
187
+ kwargs = {}
188
+ for macro_field in fields(Self):
189
+ views = state.get(macro_field.name)
190
+ if views is None:
191
+ if macro_field.default_factory is not MISSING:
192
+ kwargs[macro_field.name] = macro_field.default_factory()
193
+ elif macro_field.default is not MISSING:
194
+ kwargs[macro_field.name] = macro_field.default
195
+ else: # gds or lef
196
+ raise ValueError(
197
+ f"Macro cannot be made out of input state: View {macro_field.name} is missing"
198
+ )
199
+ continue
200
+ var_name = f"{Self.__name__}.{macro_field.name}"
201
+ _, final = Variable(var_name, macro_field.type, "").compile(
202
+ GenericDict({var_name: views}),
203
+ warning_list_ref=[],
204
+ permissive_typing=True,
205
+ )
206
+ kwargs[macro_field.name] = final
207
+
208
+ return Self(**kwargs) # type: ignore
209
+
210
+ def instantiate(
211
+ self,
212
+ instance_name: str,
213
+ location: Tuple[Number, Number],
214
+ orientation: Orientation = Orientation.N,
215
+ ):
216
+ location = (Decimal(location[0]), Decimal(location[1]))
217
+ self.instances[instance_name] = Instance(location, Orientation[orientation])
218
+
219
+
220
+ def is_optional(t: Type[Any]) -> bool:
221
+ type_args = get_args(t)
222
+ return get_origin(t) is Union and type(None) in type_args
223
+
224
+
225
+ def some_of(t: Type[Any]) -> Type[Any]:
226
+ if not is_optional(t):
227
+ return t
228
+
229
+ # t must be a Union with None if we're here
230
+
231
+ type_args = get_args(t)
232
+
233
+ args_without_none = [arg for arg in type_args if arg != type(None)]
234
+ if len(args_without_none) == 1:
235
+ return args_without_none[0]
236
+
237
+ new_union = Union[tuple(args_without_none)] # type: ignore
238
+ return new_union # type: ignore
239
+
240
+
241
+ def repr_type(t: Type[Any], for_document: bool = False) -> str: # pragma: no cover
242
+ optional = is_optional(t)
243
+ some = some_of(t)
244
+
245
+ if hasattr(some, "__name__"): # Python 3.10+
246
+ type_string = some.__name__
247
+ else:
248
+ type_string = str(some)
249
+
250
+ if is_dataclass(t):
251
+ type_string = (
252
+ f"{{class}}`{some.__qualname__} <{some.__module__}.{some.__qualname__}>`"
253
+ )
254
+
255
+ separator = "|<br />" if for_document else "|"
256
+
257
+ if inspect.isclass(some) and issubclass(some, Enum):
258
+ type_string = separator.join([str(e.name) for e in some])
259
+ type_string = f"`{type_string}`"
260
+ else:
261
+ origin, args = get_origin(some), get_args(some)
262
+ if origin is not None:
263
+ if origin == Union:
264
+ arg_strings = [repr_type(arg) for arg in args]
265
+ type_string = separator.join(arg_strings)
266
+ type_string = f"({type_string})"
267
+ elif origin == Literal:
268
+ return separator.join([repr(arg) for arg in args])
269
+ else:
270
+ arg_strings = [repr_type(arg) for arg in args]
271
+ type_string = f"{type_string}[{', '.join(arg_strings)}]"
272
+
273
+ return type_string + ("?" if optional else "")
274
+
275
+
276
+ @dataclass
277
+ class Variable:
278
+ """
279
+ An object encapsulating metadata on an LibreLane configuration variable, which
280
+ is used to name, document and validate values supplied to
281
+ :class:`librelane.steps.Step`\\s or :class:`librelane.flows.Flow`\\s.
282
+
283
+ Values supplied for configuration variables are the primary interface by
284
+ which users configure LibreLane flows.
285
+
286
+ :param name: A string name for the Variable. Because of backwards compatibility
287
+ with LibreLane 1, the convention is ``UPPER_SNAKE_CASE``.
288
+
289
+ :param type: A Python type object representing the variable.
290
+
291
+ Supported scalars:
292
+
293
+ - ``int``
294
+ - ``decimal.Decimal``
295
+ - ``bool``
296
+ - ``str``
297
+ - :class:`Path`
298
+
299
+ Supported products:
300
+
301
+ - ``Union`` (incl. ``Optional``)
302
+ - ``List``
303
+ - ``Tuple``
304
+ - ``Dict``
305
+ - ``Enum``
306
+
307
+ Other:
308
+
309
+ - ``dataclass`` types composed of the above.
310
+
311
+ :param description: A human-readable description of the variable. Used to
312
+ generate help strings and documentation.
313
+
314
+ :param default: A default value for the variable.
315
+
316
+ Optional variables have an implicit default value of ``None``.
317
+
318
+ :param deprecated_names: A list of deprecated names for said variable.
319
+
320
+ An element of the list can alternative be a tuple of a name and a Callable
321
+ used to perform a translation for when a renamed variable is also slightly
322
+ modified.
323
+
324
+ :param units: Used only in documentation: the unit corresponding to this
325
+ object, i.e., µm, pF, etc. Can be any string, but for consistency, SI units
326
+ must be represented in terms of their official symbols.
327
+
328
+ :param pdk: Whether this variable is expected to be given a default value
329
+ by a PDK or not.
330
+
331
+ If this is true, and the variable is not of an option type, a PDK *must*
332
+ give this variable a default value in order to be marked compatible
333
+ with a step.
334
+
335
+ If this is true and the variable is of an option type, a PDK may
336
+ optionally provide a default value for this variable, however steps
337
+ must presume it is ``null``.
338
+
339
+ If this is false, a PDK is not allowed to set a default value for
340
+ this variable. In current versions of LibreLane, the value will be
341
+ silently ignored, but warnings or errors may occur in future versions.
342
+ """
343
+
344
+ known_variable_names: ClassVar[Set[str]] = set()
345
+
346
+ name: str
347
+ type: Any
348
+ description: str
349
+ default: Any = None
350
+ deprecated_names: List[Union[str, Tuple[str, Callable]]] = field(
351
+ default_factory=list
352
+ )
353
+
354
+ units: Optional[str] = None
355
+ pdk: bool = False
356
+
357
+ def __post_init__(self):
358
+ Variable.known_variable_names.add(self.name)
359
+ for name in self.deprecated_names:
360
+ if isinstance(name, tuple):
361
+ name, _ = name
362
+ Variable.known_variable_names.add(name)
363
+
364
+ @property
365
+ def optional(self) -> bool:
366
+ """
367
+ :returns: Whether a variable's type is an `Option type <https://en.wikipedia.org/wiki/Option_type>`_.
368
+ """
369
+ return is_optional(self.type)
370
+
371
+ @property
372
+ def some(self) -> Any:
373
+ """
374
+ :returns: The type of a variable presuming it is not None.
375
+
376
+ If a variable is not Optional, that is simply the type specified in the
377
+ ``type`` attribute.
378
+ """
379
+ return some_of(self.type)
380
+
381
+ def type_repr_md(self, for_document: bool = False) -> str: # pragma: no cover
382
+ """
383
+ :param for_document: Adds HTML line breaks between sum type separators
384
+ for easier wrapping by web browsers/PDF renderers/what have you
385
+ :returns: A pretty Markdown string representation of the Variable's type.
386
+ """
387
+ return repr_type(self.type, for_document=for_document)
388
+
389
+ def desc_repr_md(self) -> str: # pragma: no cover
390
+ """
391
+ :returns: The description, but with newlines escaped for Markdown.
392
+ """
393
+ return self.description.replace("\n", "<br />")
394
+
395
+ def __process(
396
+ self,
397
+ key_path: str,
398
+ value: Any,
399
+ validating_type: Type[Any],
400
+ default: Any = None,
401
+ explicitly_specified: bool = True,
402
+ permissive_typing: bool = False,
403
+ depth: int = 0,
404
+ ):
405
+ if value is None:
406
+ if explicitly_specified:
407
+ # User explicitly specified "null" for this value: only error if
408
+ # value is not optional
409
+ if not is_optional(validating_type):
410
+ raise ValueError(
411
+ f"Non-optional variable '{key_path}' explicitly assigned a null value."
412
+ )
413
+ else:
414
+ return None
415
+ else:
416
+ # User did not specify a value for this variable: couple outcomes
417
+ if default is not None:
418
+ return self.__process(
419
+ key_path=key_path,
420
+ value=default,
421
+ validating_type=validating_type,
422
+ permissive_typing=permissive_typing,
423
+ depth=depth + 1,
424
+ )
425
+ elif not is_optional(validating_type):
426
+ if depth == 0:
427
+ raise MissingRequiredVariable(self, self.pdk)
428
+ else:
429
+ raise ValueError(f"'{key_path}' must be non-null.")
430
+ else:
431
+ return None
432
+
433
+ if is_optional(validating_type):
434
+ validating_type = some_of(validating_type)
435
+
436
+ type_origin = get_origin(validating_type)
437
+ type_args = get_args(validating_type)
438
+
439
+ if type_origin in [list, tuple]:
440
+ return_value = list()
441
+ raw = value
442
+ if isinstance(raw, list) or isinstance(raw, tuple):
443
+ if validating_type == List[Path]:
444
+ if any(isinstance(item, List) for item in raw):
445
+ Variable.__flatten_list(value)
446
+ pass
447
+ elif is_string(raw):
448
+ if not permissive_typing:
449
+ raise ValueError(
450
+ f"Refusing to automatically convert string at '{key_path}' to list"
451
+ )
452
+ if "," in raw:
453
+ raw = raw.split(",")
454
+ elif ";" in raw:
455
+ raw = raw.split(";")
456
+ else:
457
+ raw = raw.split()
458
+ if len(raw) and raw[-1] == "":
459
+ raw.pop() # Trailing commas
460
+ else:
461
+ raise ValueError(
462
+ f"List provided for variable '{key_path}' is invalid: {value}"
463
+ )
464
+
465
+ if type_origin == tuple:
466
+ if len(raw) != len(type_args):
467
+ raise ValueError(
468
+ f"Value provided for variable '{key_path}' of type {validating_type} is invalid: ({len(raw)}/{len(type_args)}) tuple entries provided"
469
+ )
470
+
471
+ for i, (item, value_type) in enumerate(
472
+ zip_first(raw, type_args, fillvalue=type_args[0])
473
+ ):
474
+ return_value.append(
475
+ self.__process(
476
+ key_path=f"{key_path}[{i}]",
477
+ value=item,
478
+ validating_type=value_type,
479
+ permissive_typing=permissive_typing,
480
+ depth=depth + 1,
481
+ )
482
+ )
483
+
484
+ if type_origin == tuple:
485
+ return tuple(return_value)
486
+
487
+ return return_value
488
+ elif type_origin == dict:
489
+ raw = value
490
+ key_type, value_type = type_args
491
+ if isinstance(raw, dict):
492
+ pass
493
+ elif isinstance(raw, list) or is_string(raw):
494
+ if not permissive_typing:
495
+ raise ValueError(
496
+ f"Refusing to automatically convert string at '{key_path}' to dict"
497
+ )
498
+ components = raw
499
+ if is_string(raw):
500
+ components = shlex.split(raw)
501
+ assert isinstance(components, list)
502
+ # Assuming Tcl format:
503
+ if len(components) % 2 != 0:
504
+ raise ValueError(
505
+ f"Tcl-style flat dictionary provided for variable '{key_path}' is invalid: uneven number of components ({len(components)})"
506
+ )
507
+ raw = {}
508
+ for i in range(0, len(components) // 2):
509
+ key = components[2 * i]
510
+ val = components[2 * i + 1]
511
+ raw[key] = val
512
+ else:
513
+ raise ValueError(
514
+ f"Value provided for variable '{key_path}' of type {validating_type} is invalid: '{value}'"
515
+ )
516
+
517
+ processed = {}
518
+ for key, val in raw.items():
519
+ key_validated = self.__process(
520
+ key_path=key_path,
521
+ value=key,
522
+ validating_type=key_type,
523
+ permissive_typing=permissive_typing,
524
+ depth=depth + 1,
525
+ )
526
+ value_validated = self.__process(
527
+ key_path=f"{key_path}.{key_validated}",
528
+ value=val,
529
+ validating_type=value_type,
530
+ permissive_typing=permissive_typing,
531
+ depth=depth + 1,
532
+ )
533
+ processed[key_validated] = value_validated
534
+
535
+ return processed
536
+ elif type_origin == Union:
537
+ final_value = None
538
+ errors = []
539
+ for arg in type_args:
540
+ try:
541
+ final_value = self.__process(
542
+ key_path=key_path,
543
+ value=value,
544
+ validating_type=arg,
545
+ permissive_typing=permissive_typing,
546
+ depth=depth + 1,
547
+ )
548
+ if final_value is not None:
549
+ return final_value
550
+ except ValueError as e:
551
+ errors.append(f"\t{str(e)}")
552
+ raise ValueError(
553
+ "\n".join(
554
+ [
555
+ f"Value for '{key_path}' is invalid for union {repr_type(validating_type)}:"
556
+ ]
557
+ + errors
558
+ )
559
+ )
560
+ elif type_origin == Literal:
561
+ if value in type_args:
562
+ return value
563
+ else:
564
+ raise ValueError(
565
+ f"Value for '{key_path}' is invalid for {repr_type(validating_type)}: '{value}'"
566
+ )
567
+ elif is_dataclass(validating_type):
568
+ if isinstance(value, validating_type):
569
+ # Do not validate further
570
+ return value
571
+
572
+ raw = value
573
+ if not isinstance(raw, dict):
574
+ raise ValueError(
575
+ f"Value provided for deserializable class {validating_type} at '{key_path}' is not a dictionary."
576
+ )
577
+ raw = value.copy()
578
+ kwargs_dict = {}
579
+ for current_field in fields(validating_type):
580
+ key = current_field.name
581
+ subtype = current_field.type
582
+ explicitly_specified = False
583
+ if key in raw:
584
+ explicitly_specified = True
585
+ field_value = raw.get(key)
586
+ field_default = None
587
+ if (
588
+ current_field.default is not None
589
+ and type(current_field.default) != _MISSING_TYPE
590
+ ):
591
+ field_default = current_field.default
592
+ if current_field.default_factory != MISSING:
593
+ field_default = current_field.default_factory()
594
+ value__processed = self.__process(
595
+ key_path=f"{key_path}.{key}",
596
+ value=field_value,
597
+ explicitly_specified=explicitly_specified,
598
+ default=field_default,
599
+ validating_type=subtype,
600
+ permissive_typing=permissive_typing,
601
+ depth=depth + 1,
602
+ )
603
+ kwargs_dict[key] = value__processed
604
+ if explicitly_specified:
605
+ del raw[key]
606
+ if len(raw):
607
+ raise ValueError(
608
+ f"One or more keys unrecognized for dataclass {validating_type.__qualname__}: {' '.join(raw.keys())}"
609
+ )
610
+ return validating_type(**kwargs_dict)
611
+ elif validating_type == Path:
612
+ # Handle one-file globs
613
+ if isinstance(value, list) and len(value) == 1:
614
+ value = value[0]
615
+ result = Path(value)
616
+ result.validate(f"Path provided for variable '{key_path}' is invalid")
617
+ return result
618
+ elif validating_type == bool:
619
+ if not permissive_typing and not isinstance(value, bool):
620
+ raise ValueError(
621
+ f"Refusing to automatically convert '{value}' at '{key_path}' to a Boolean"
622
+ )
623
+ if value in ["1", "true", "True", 1, True]:
624
+ return True
625
+ elif value in ["0", "false", "False", 0, False]:
626
+ return False
627
+ else:
628
+ raise ValueError(
629
+ f"Value provided for variable '{key_path}' of type {validating_type.__name__} is invalid: '{value}'"
630
+ )
631
+ elif issubclass(validating_type, Enum):
632
+ if type(value) == validating_type:
633
+ return value
634
+ try:
635
+ return validating_type[value]
636
+ except KeyError:
637
+ raise ValueError(
638
+ f"Variable provided for variable '{key_path}' of enumerated type {validating_type.__name__} is invalid: '{value}'"
639
+ )
640
+ elif issubclass(validating_type, str):
641
+ if not is_string(value):
642
+ raise ValueError(
643
+ f"Refusing to automatically convert value at '{key_path}' to a string"
644
+ )
645
+ return str(value)
646
+ elif issubclass(validating_type, Decimal) or issubclass(validating_type, int):
647
+ try:
648
+ final = validating_type(value)
649
+ except (InvalidOperation, TypeError):
650
+ raise ValueError(
651
+ f"Value provided for variable '{key_path}' of type {validating_type.__name__} is invalid: '{value}'"
652
+ )
653
+ if not permissive_typing and not (
654
+ isinstance(value, int)
655
+ or isinstance(value, float)
656
+ or isinstance(value, Decimal)
657
+ ):
658
+ raise ValueError(
659
+ f"Refusing to automatically convert value at '{key_path}' to a {validating_type.__name__}"
660
+ )
661
+ return final
662
+
663
+ else:
664
+ try:
665
+ return validating_type(value)
666
+ except ValueError as e:
667
+ raise ValueError(
668
+ f"Value provided for variable '{key_path}' of type {validating_type.__name__} is invalid: '{value}' {e}"
669
+ )
670
+
671
+ def compile(
672
+ self,
673
+ mutable_config: GenericDict[str, Any],
674
+ warning_list_ref: List[str],
675
+ values_so_far: Optional[Mapping[str, Any]] = None,
676
+ permissive_typing: bool = False,
677
+ ) -> Tuple[Optional[str], Any]:
678
+ exists: Optional[str] = None
679
+ value: Optional[Any] = None
680
+
681
+ i = 0
682
+ while (
683
+ not exists
684
+ and self.deprecated_names is not None
685
+ and i < len(self.deprecated_names)
686
+ ):
687
+ deprecated_name = self.deprecated_names[i]
688
+ deprecated_callable = lambda x: x
689
+ if not isinstance(deprecated_name, str):
690
+ deprecated_name, deprecated_callable = deprecated_name
691
+ exists, value = mutable_config.check(deprecated_name)
692
+ if exists:
693
+ warning_list_ref.append(
694
+ f"The configuration variable '{deprecated_name}' is deprecated. Please check the docs for the usage on the replacement variable '{self.name}'."
695
+ )
696
+ if value is not None:
697
+ value = deprecated_callable(value)
698
+ i = i + 1
699
+
700
+ if not exists:
701
+ exists, value = mutable_config.check(self.name)
702
+
703
+ processed = self.__process(
704
+ key_path=self.name,
705
+ value=value,
706
+ default=self.default,
707
+ validating_type=self.type,
708
+ explicitly_specified=exists is not None,
709
+ permissive_typing=permissive_typing,
710
+ )
711
+
712
+ return (exists, processed)
713
+
714
+ def _get_docs_identifier(self, parent: Optional[str] = None) -> str:
715
+ identifier = f"var-{self.name.lower()}"
716
+ if parent is not None:
717
+ identifier = f"var-{slugify(parent)}-{self.name.lower()}"
718
+ return identifier
719
+
720
+ def __hash__(self) -> int:
721
+ return hash((self.name, self.type, self.default))
722
+
723
+ def __eq__(self, rhs: object) -> bool:
724
+ if not isinstance(rhs, Variable):
725
+ raise NotImplementedError()
726
+ return (
727
+ self.name == rhs.name
728
+ and self.type == rhs.type
729
+ and self.default == rhs.default
730
+ )
731
+
732
+ # Flatten list. Note: Must modify value, not return a new list.
733
+ @staticmethod
734
+ def __flatten_list(value: list):
735
+ new_list = []
736
+ for item in value:
737
+ if isinstance(item, list):
738
+ for sub_item in item:
739
+ new_list.append(sub_item)
740
+ else:
741
+ new_list.append(item)
742
+
743
+ value[:] = new_list