librelane 2.4.0.dev2__py3-none-any.whl → 2.4.7__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.
- librelane/__init__.py +1 -1
- librelane/__main__.py +34 -27
- librelane/common/__init__.py +2 -0
- librelane/common/cli.py +1 -1
- librelane/common/drc.py +1 -0
- librelane/common/generic_dict.py +1 -1
- librelane/common/metrics/__main__.py +1 -1
- librelane/common/misc.py +58 -2
- librelane/common/tcl.py +2 -1
- librelane/common/types.py +2 -3
- librelane/config/__main__.py +1 -4
- librelane/config/flow.py +2 -2
- librelane/config/preprocessor.py +1 -1
- librelane/config/variable.py +136 -7
- librelane/container.py +55 -31
- librelane/env_info.py +129 -115
- librelane/examples/hold_eco_demo/config.yaml +18 -0
- librelane/examples/hold_eco_demo/demo.v +27 -0
- librelane/flows/cli.py +39 -23
- librelane/flows/flow.py +100 -36
- librelane/help/__main__.py +39 -0
- librelane/scripts/magic/def/mag_gds.tcl +0 -2
- librelane/scripts/magic/drc.tcl +0 -1
- librelane/scripts/magic/gds/extras_mag.tcl +0 -2
- librelane/scripts/magic/gds/mag_with_pointers.tcl +0 -1
- librelane/scripts/magic/lef/extras_maglef.tcl +0 -2
- librelane/scripts/magic/lef/maglef.tcl +0 -1
- librelane/scripts/magic/wrapper.tcl +2 -0
- librelane/scripts/odbpy/defutil.py +15 -10
- librelane/scripts/odbpy/eco_buffer.py +182 -0
- librelane/scripts/odbpy/eco_diode.py +140 -0
- librelane/scripts/odbpy/ioplace_parser/__init__.py +1 -1
- librelane/scripts/odbpy/ioplace_parser/parse.py +1 -1
- librelane/scripts/odbpy/power_utils.py +8 -6
- librelane/scripts/odbpy/reader.py +17 -13
- librelane/scripts/openroad/common/io.tcl +66 -2
- librelane/scripts/openroad/gui.tcl +23 -1
- librelane/state/design_format.py +16 -1
- librelane/state/state.py +11 -3
- librelane/steps/__init__.py +1 -1
- librelane/steps/__main__.py +4 -4
- librelane/steps/checker.py +7 -8
- librelane/steps/klayout.py +11 -1
- librelane/steps/magic.py +24 -14
- librelane/steps/misc.py +5 -0
- librelane/steps/odb.py +193 -28
- librelane/steps/openroad.py +64 -47
- librelane/steps/pyosys.py +18 -1
- librelane/steps/step.py +36 -17
- librelane/steps/yosys.py +9 -1
- {librelane-2.4.0.dev2.dist-info → librelane-2.4.7.dist-info}/METADATA +10 -11
- {librelane-2.4.0.dev2.dist-info → librelane-2.4.7.dist-info}/RECORD +54 -50
- {librelane-2.4.0.dev2.dist-info → librelane-2.4.7.dist-info}/entry_points.txt +1 -0
- librelane/scripts/odbpy/exception_codes.py +0 -17
- {librelane-2.4.0.dev2.dist-info → librelane-2.4.7.dist-info}/WHEEL +0 -0
librelane/__init__.py
CHANGED
librelane/__main__.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Copyright 2025
|
|
1
|
+
# Copyright 2025 LibreLane Contributors
|
|
2
2
|
#
|
|
3
3
|
# Adapted from OpenLane
|
|
4
4
|
#
|
|
@@ -22,7 +22,6 @@ import shutil
|
|
|
22
22
|
import marshal
|
|
23
23
|
import tempfile
|
|
24
24
|
import traceback
|
|
25
|
-
import subprocess
|
|
26
25
|
from textwrap import dedent
|
|
27
26
|
from functools import partial
|
|
28
27
|
from typing import Any, Dict, Sequence, Tuple, Type, Optional, List
|
|
@@ -221,7 +220,10 @@ def print_version(ctx: click.Context, param: click.Parameter, value: bool):
|
|
|
221
220
|
if len(discovered_plugins) > 0:
|
|
222
221
|
print("Discovered plugins:")
|
|
223
222
|
for name, module in discovered_plugins.items():
|
|
224
|
-
|
|
223
|
+
if hasattr(module, "__version__"):
|
|
224
|
+
print(f"{name} -> {module.__version__}")
|
|
225
|
+
else:
|
|
226
|
+
print(f"{name}")
|
|
225
227
|
|
|
226
228
|
ctx.exit(0)
|
|
227
229
|
|
|
@@ -278,20 +280,12 @@ def run_included_example(
|
|
|
278
280
|
if os.path.isdir(final_path):
|
|
279
281
|
print(f"A directory named {value} already exists.", file=sys.stderr)
|
|
280
282
|
ctx.exit(1)
|
|
281
|
-
# 1. Copy the files
|
|
282
|
-
shutil.copytree(
|
|
283
|
-
example_path,
|
|
284
|
-
final_path,
|
|
285
|
-
symlinks=False,
|
|
286
|
-
)
|
|
287
|
-
|
|
288
|
-
# 2. Make files writable
|
|
289
|
-
if os.name == "posix":
|
|
290
|
-
subprocess.check_call(["chmod", "-R", "755", final_path])
|
|
291
283
|
|
|
284
|
+
# 1. Copy the files
|
|
285
|
+
common.recreate_tree(example_path, final_path)
|
|
292
286
|
config_file = glob.glob(os.path.join(final_path, "config.*"))[0]
|
|
293
287
|
|
|
294
|
-
#
|
|
288
|
+
# 2. Run
|
|
295
289
|
run(
|
|
296
290
|
ctx,
|
|
297
291
|
config_files=[config_file],
|
|
@@ -321,28 +315,38 @@ def cli_in_container(
|
|
|
321
315
|
if not value:
|
|
322
316
|
return
|
|
323
317
|
|
|
324
|
-
|
|
325
|
-
|
|
318
|
+
mounts = list(ctx.params.get("docker_mounts") or ())
|
|
319
|
+
tty: bool = ctx.params.get("docker_tty", True)
|
|
326
320
|
pdk_root = ctx.params.get("pdk_root")
|
|
327
|
-
|
|
321
|
+
|
|
322
|
+
try:
|
|
323
|
+
containerized_index = sys.argv.index("--dockerized")
|
|
324
|
+
except ValueError:
|
|
325
|
+
containerized_index = sys.argv.index("--containerized")
|
|
326
|
+
|
|
327
|
+
argv = sys.argv[containerized_index + 1 :]
|
|
328
328
|
|
|
329
329
|
final_argv = ["zsh"]
|
|
330
330
|
if len(argv) != 0:
|
|
331
|
-
final_argv = ["librelane"] + argv
|
|
331
|
+
final_argv = ["python3", "-m", "librelane"] + argv
|
|
332
332
|
|
|
333
|
-
|
|
333
|
+
container_image = os.getenv(
|
|
334
334
|
"LIBRELANE_IMAGE_OVERRIDE", f"ghcr.io/librelane/librelane:{__version__}"
|
|
335
335
|
)
|
|
336
336
|
|
|
337
337
|
try:
|
|
338
338
|
run_in_container(
|
|
339
|
-
|
|
339
|
+
container_image,
|
|
340
340
|
final_argv,
|
|
341
341
|
pdk_root=pdk_root,
|
|
342
|
-
other_mounts=
|
|
343
|
-
tty=
|
|
342
|
+
other_mounts=mounts,
|
|
343
|
+
tty=tty,
|
|
344
344
|
)
|
|
345
|
+
except ValueError as e:
|
|
346
|
+
err(e)
|
|
347
|
+
ctx.exit(1)
|
|
345
348
|
except Exception as e:
|
|
349
|
+
traceback.print_exc(file=sys.stderr)
|
|
346
350
|
err(e)
|
|
347
351
|
ctx.exit(1)
|
|
348
352
|
|
|
@@ -377,25 +381,28 @@ o = partial(option, show_default=True)
|
|
|
377
381
|
"Containerization options",
|
|
378
382
|
o(
|
|
379
383
|
"--docker-mount",
|
|
384
|
+
"--container-mount",
|
|
380
385
|
"-m",
|
|
381
386
|
"docker_mounts",
|
|
382
387
|
multiple=True,
|
|
383
|
-
is_eager=True, #
|
|
388
|
+
is_eager=True, # container options should be processed before anything else
|
|
384
389
|
default=(),
|
|
385
|
-
help="Used to mount more directories in dockerized mode. If a valid directory is specified, it will be mounted in the same path in the container. Otherwise, the value of the option will be passed to the
|
|
390
|
+
help="Used to mount more directories in dockerized mode. If a valid directory is specified, it will be mounted in the same path in the container. Otherwise, the value of the option will be passed to the container engine verbatim. Must be passed before --containerized/--dockerized, has no effect if not set.",
|
|
386
391
|
),
|
|
387
392
|
o(
|
|
388
393
|
"--docker-tty/--docker-no-tty",
|
|
389
|
-
|
|
394
|
+
"--container-tty/--container-no-tty",
|
|
395
|
+
is_eager=True, # container options should be processed before anything else
|
|
390
396
|
default=True,
|
|
391
|
-
help="Controls the allocation of a virtual terminal by passing -t to the Docker-compatible container engine invocation. Must be passed before --dockerized, has no effect if
|
|
397
|
+
help="Controls the allocation of a virtual terminal by passing -t to the Docker-compatible container engine invocation. Must be passed before --containerized/--dockerized, has no effect if not set.",
|
|
392
398
|
),
|
|
393
399
|
o(
|
|
394
400
|
"--dockerized",
|
|
401
|
+
"--containerized",
|
|
395
402
|
default=False,
|
|
396
403
|
is_flag=True,
|
|
397
404
|
is_eager=True, # docker options should be processed before anything else
|
|
398
|
-
help="Run the remaining flags using a
|
|
405
|
+
help="Run the remaining flags using a containerized version of LibreLane. Some caveats apply. Must precede all options except --{docker,container}-mount, --{docker,container}-[no-]tty.",
|
|
399
406
|
callback=cli_in_container,
|
|
400
407
|
),
|
|
401
408
|
)
|
librelane/common/__init__.py
CHANGED
librelane/common/cli.py
CHANGED
|
@@ -67,7 +67,7 @@ class IntEnumChoice(Choice):
|
|
|
67
67
|
f"{value} is not a not a valid value for IntEnum {self.__enum.__name__}"
|
|
68
68
|
)
|
|
69
69
|
|
|
70
|
-
def get_metavar(self, param:
|
|
70
|
+
def get_metavar(self, param: Parameter) -> str:
|
|
71
71
|
_bk = self.choices
|
|
72
72
|
self.choices = [f"{e.name} or {e.value}" for e in self.__enum]
|
|
73
73
|
result = super().get_metavar(param)
|
librelane/common/drc.py
CHANGED
librelane/common/generic_dict.py
CHANGED
|
@@ -69,7 +69,7 @@ VT = TypeVar("VT")
|
|
|
69
69
|
|
|
70
70
|
class GenericDict(Mapping[KT, VT]):
|
|
71
71
|
"""
|
|
72
|
-
A dictionary with generic keys and values that is compatible with Python 3.8.
|
|
72
|
+
A dictionary with generic keys and values that is compatible with Python 3.8.1.
|
|
73
73
|
|
|
74
74
|
:param copying: A base Mapping object to copy values from.
|
|
75
75
|
:param overrides: Another mapping object to override the value from `copying`
|
librelane/common/misc.py
CHANGED
|
@@ -11,16 +11,19 @@
|
|
|
11
11
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
|
+
import io
|
|
14
15
|
import os
|
|
15
16
|
import re
|
|
16
17
|
import glob
|
|
17
18
|
import gzip
|
|
19
|
+
import shutil
|
|
18
20
|
import typing
|
|
19
21
|
import fnmatch
|
|
20
22
|
import pathlib
|
|
21
23
|
import unicodedata
|
|
22
24
|
from math import inf
|
|
23
25
|
from typing import (
|
|
26
|
+
IO,
|
|
24
27
|
Any,
|
|
25
28
|
Generator,
|
|
26
29
|
Iterable,
|
|
@@ -109,6 +112,8 @@ def get_opdks_rev() -> str:
|
|
|
109
112
|
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
110
113
|
def slugify(value: str, lower: bool = False) -> str:
|
|
111
114
|
"""
|
|
115
|
+
Adapted from Django slugify. In practice it works more like a kebabify…
|
|
116
|
+
|
|
112
117
|
:param value: Input string
|
|
113
118
|
:returns: The input string converted to lower case, with all characters
|
|
114
119
|
except alphanumerics, underscores and hyphens removed, and spaces and\
|
|
@@ -314,6 +319,37 @@ class Filter(object):
|
|
|
314
319
|
yield input
|
|
315
320
|
|
|
316
321
|
|
|
322
|
+
def recreate_tree(
|
|
323
|
+
source: AnyPath,
|
|
324
|
+
target: AnyPath,
|
|
325
|
+
):
|
|
326
|
+
"""
|
|
327
|
+
This function attempts to recreate a file tree from a source path in another
|
|
328
|
+
target path.
|
|
329
|
+
|
|
330
|
+
Permissions are not copied over. Symlinks and hardlinks are followed.
|
|
331
|
+
|
|
332
|
+
Directories are not recreated unless they contain files as (grand)children.
|
|
333
|
+
|
|
334
|
+
If the source and target are the same, the function returns early and does
|
|
335
|
+
nothing.
|
|
336
|
+
|
|
337
|
+
:param source: The source file tree to replicate
|
|
338
|
+
:param target: The target path to recreate the file tree within
|
|
339
|
+
"""
|
|
340
|
+
source = os.path.abspath(source)
|
|
341
|
+
target = os.path.abspath(target)
|
|
342
|
+
if os.path.exists(target) and os.path.samefile(source, target):
|
|
343
|
+
return
|
|
344
|
+
for dirname, _, files in os.walk(source):
|
|
345
|
+
for file in files:
|
|
346
|
+
resolved = os.path.join(dirname, file)
|
|
347
|
+
resolved_target = os.path.join(target, os.path.relpath(resolved, source))
|
|
348
|
+
os.makedirs(os.path.dirname(resolved_target), exist_ok=True)
|
|
349
|
+
with open(resolved, "rb") as fi, open(resolved_target, "wb") as fo:
|
|
350
|
+
shutil.copyfileobj(fi, fo)
|
|
351
|
+
|
|
352
|
+
|
|
317
353
|
def get_latest_file(in_path: Union[str, os.PathLike], filename: str) -> Optional[Path]:
|
|
318
354
|
"""
|
|
319
355
|
:param in_path: A directory to search in
|
|
@@ -378,9 +414,9 @@ def _get_process_limit() -> int:
|
|
|
378
414
|
return int(os.getenv("_OPENLANE_MAX_CORES", os.cpu_count() or 1))
|
|
379
415
|
|
|
380
416
|
|
|
381
|
-
def gzopen(filename, mode="rt"):
|
|
417
|
+
def gzopen(filename: AnyPath, mode="rt") -> IO[Any]:
|
|
382
418
|
"""
|
|
383
|
-
This
|
|
419
|
+
This function (tries to?) emulate the gzopen from the Linux Standard Base,
|
|
384
420
|
specifically this part:
|
|
385
421
|
|
|
386
422
|
If path refers to an uncompressed file, and mode refers to a read mode,
|
|
@@ -388,6 +424,11 @@ def gzopen(filename, mode="rt"):
|
|
|
388
424
|
for reading directly from the file without any decompression.
|
|
389
425
|
|
|
390
426
|
gzip.open does not have this behavior.
|
|
427
|
+
|
|
428
|
+
:param filename: The full path to the uncompressed or gzipped file.
|
|
429
|
+
:param mode: "r", "rb", "w", "wb", "x", "xb", "a" or "ab" for
|
|
430
|
+
binary mode, or "rt", "wt", "xt" or "at" for text mode.
|
|
431
|
+
:returns: An I/O wrapper that may very slightly based on the mode.
|
|
391
432
|
"""
|
|
392
433
|
try:
|
|
393
434
|
g = gzip.open(filename, mode=mode)
|
|
@@ -400,3 +441,18 @@ def gzopen(filename, mode="rt"):
|
|
|
400
441
|
except gzip.BadGzipFile:
|
|
401
442
|
g.close()
|
|
402
443
|
return open(filename, mode=mode)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def count_occurences(fp: io.TextIOWrapper, pattern: str = "") -> int:
|
|
447
|
+
"""
|
|
448
|
+
Counts the occurences of a certain string in a stream, line-by-line, without
|
|
449
|
+
necessarily loading the entire file into memory.
|
|
450
|
+
|
|
451
|
+
Equivalent to: ``grep -c 'pattern' <file>`` (but without regex support).
|
|
452
|
+
|
|
453
|
+
:param fp: the text stream
|
|
454
|
+
:param pattern: the substring to search for. if set to "", it will simply
|
|
455
|
+
count the lines in the file.
|
|
456
|
+
:returns: the number of matching lines
|
|
457
|
+
"""
|
|
458
|
+
return sum(pattern in line for line in fp)
|
librelane/common/tcl.py
CHANGED
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
import re
|
|
15
|
-
import tkinter
|
|
16
15
|
from typing import Dict, Mapping, Any, Iterable
|
|
17
16
|
|
|
18
17
|
_env_rx = re.compile(r"(?:\:\:)?env\((\w+)\)")
|
|
@@ -55,6 +54,8 @@ class TclUtils(object):
|
|
|
55
54
|
|
|
56
55
|
@staticmethod
|
|
57
56
|
def _eval_env(env_in: Mapping[str, Any], tcl_in: str) -> Dict[str, Any]:
|
|
57
|
+
import tkinter
|
|
58
|
+
|
|
58
59
|
interpreter = tkinter.Tcl()
|
|
59
60
|
|
|
60
61
|
interpreter.eval("array unset ::env")
|
librelane/common/types.py
CHANGED
|
@@ -16,6 +16,7 @@ import sys
|
|
|
16
16
|
import tempfile
|
|
17
17
|
from math import isfinite
|
|
18
18
|
from decimal import Decimal
|
|
19
|
+
from weakref import finalize
|
|
19
20
|
from collections import UserString
|
|
20
21
|
from typing import Any, Union, ClassVar, Tuple, Optional
|
|
21
22
|
|
|
@@ -112,6 +113,4 @@ class ScopedFile(Path):
|
|
|
112
113
|
super().__init__(self._ntf.name)
|
|
113
114
|
self._ntf.write(contents)
|
|
114
115
|
self._ntf.close()
|
|
115
|
-
|
|
116
|
-
def __del__(self):
|
|
117
|
-
os.unlink(self._ntf.name)
|
|
116
|
+
self._ntf_cleanup = finalize(self, os.unlink, self._ntf.name)
|
librelane/config/__main__.py
CHANGED
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
import os
|
|
15
15
|
import sys
|
|
16
16
|
import json
|
|
17
|
-
import functools
|
|
18
17
|
from decimal import Decimal
|
|
19
18
|
|
|
20
19
|
import click
|
|
@@ -111,9 +110,7 @@ def create_config(
|
|
|
111
110
|
print("At least one source RTL file is required.", file=sys.stderr)
|
|
112
111
|
exit(1)
|
|
113
112
|
source_rtl_key = "VERILOG_FILES"
|
|
114
|
-
if not
|
|
115
|
-
lambda acc, x: acc and (x.endswith(".sv") or x.endswith(".v")), source_rtl, True
|
|
116
|
-
):
|
|
113
|
+
if not all(file.endswith(".sv") or file.endswith(".v") for file in source_rtl):
|
|
117
114
|
print(
|
|
118
115
|
"Only Verilog/SystemVerilog files are supported by create-config.",
|
|
119
116
|
file=sys.stderr,
|
librelane/config/flow.py
CHANGED
|
@@ -153,13 +153,13 @@ pdk_variables = [
|
|
|
153
153
|
Variable(
|
|
154
154
|
"FP_IO_HLAYER",
|
|
155
155
|
str,
|
|
156
|
-
"The metal layer on which to place
|
|
156
|
+
"The metal layer on which to place horizontally-aligned (long side parallel with the horizon) pins alongside the east and west edges of the die.",
|
|
157
157
|
pdk=True,
|
|
158
158
|
),
|
|
159
159
|
Variable(
|
|
160
160
|
"FP_IO_VLAYER",
|
|
161
161
|
str,
|
|
162
|
-
"The metal layer on which to place
|
|
162
|
+
"The metal layer on which to place vertically-aligned (long side perpendicular to the horizon) pins alongside the north and south edges of the die.",
|
|
163
163
|
pdk=True,
|
|
164
164
|
),
|
|
165
165
|
Variable("RT_MIN_LAYER", str, "The lowest metal layer to route on.", pdk=True),
|
librelane/config/preprocessor.py
CHANGED
librelane/config/variable.py
CHANGED
|
@@ -24,9 +24,11 @@ from dataclasses import (
|
|
|
24
24
|
fields,
|
|
25
25
|
is_dataclass,
|
|
26
26
|
)
|
|
27
|
+
import textwrap
|
|
27
28
|
from typing import (
|
|
28
29
|
ClassVar,
|
|
29
30
|
Dict,
|
|
31
|
+
Iterable,
|
|
30
32
|
List,
|
|
31
33
|
Literal,
|
|
32
34
|
Optional,
|
|
@@ -238,7 +240,7 @@ def some_of(t: Type[Any]) -> Type[Any]:
|
|
|
238
240
|
return new_union # type: ignore
|
|
239
241
|
|
|
240
242
|
|
|
241
|
-
def repr_type(t: Type[Any]) -> str: # pragma: no cover
|
|
243
|
+
def repr_type(t: Type[Any], for_document: bool = False) -> str: # pragma: no cover
|
|
242
244
|
optional = is_optional(t)
|
|
243
245
|
some = some_of(t)
|
|
244
246
|
|
|
@@ -247,18 +249,25 @@ def repr_type(t: Type[Any]) -> str: # pragma: no cover
|
|
|
247
249
|
else:
|
|
248
250
|
type_string = str(some)
|
|
249
251
|
|
|
252
|
+
if is_dataclass(t):
|
|
253
|
+
type_string = (
|
|
254
|
+
f"{{class}}`{some.__qualname__} <{some.__module__}.{some.__qualname__}>`"
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
separator = "|<br />" if for_document else "|"
|
|
258
|
+
|
|
250
259
|
if inspect.isclass(some) and issubclass(some, Enum):
|
|
251
|
-
type_string =
|
|
260
|
+
type_string = separator.join([str(e.name) for e in some])
|
|
252
261
|
type_string = f"`{type_string}`"
|
|
253
262
|
else:
|
|
254
263
|
origin, args = get_origin(some), get_args(some)
|
|
255
264
|
if origin is not None:
|
|
256
265
|
if origin == Union:
|
|
257
266
|
arg_strings = [repr_type(arg) for arg in args]
|
|
258
|
-
type_string =
|
|
267
|
+
type_string = separator.join(arg_strings)
|
|
259
268
|
type_string = f"({type_string})"
|
|
260
269
|
elif origin == Literal:
|
|
261
|
-
return
|
|
270
|
+
return separator.join([repr(arg) for arg in args])
|
|
262
271
|
else:
|
|
263
272
|
arg_strings = [repr_type(arg) for arg in args]
|
|
264
273
|
type_string = f"{type_string}[{', '.join(arg_strings)}]"
|
|
@@ -377,9 +386,7 @@ class Variable:
|
|
|
377
386
|
for easier wrapping by web browsers/PDF renderers/what have you
|
|
378
387
|
:returns: A pretty Markdown string representation of the Variable's type.
|
|
379
388
|
"""
|
|
380
|
-
|
|
381
|
-
return repr_type(self.type).replace("|", "|<br />")
|
|
382
|
-
return repr_type(self.type)
|
|
389
|
+
return repr_type(self.type, for_document=for_document)
|
|
383
390
|
|
|
384
391
|
def desc_repr_md(self) -> str: # pragma: no cover
|
|
385
392
|
"""
|
|
@@ -387,6 +394,112 @@ class Variable:
|
|
|
387
394
|
"""
|
|
388
395
|
return self.description.replace("\n", "<br />")
|
|
389
396
|
|
|
397
|
+
@staticmethod
|
|
398
|
+
def _render_table_md(
|
|
399
|
+
vars: Iterable["Variable"],
|
|
400
|
+
*,
|
|
401
|
+
myst_anchor_owner_id: Optional[str] = None,
|
|
402
|
+
) -> str: # pragma; no cover
|
|
403
|
+
"""
|
|
404
|
+
Renders a markdown table for any iterable of configuration variables.
|
|
405
|
+
|
|
406
|
+
:param vars: Any iterable object returning configuration variables
|
|
407
|
+
:param myst_anchor_owner_id:
|
|
408
|
+
If set, the table is rendered for MyST using a mix of HTML and
|
|
409
|
+
Markdown, with anchors and a detail tag containing deprecated names.
|
|
410
|
+
|
|
411
|
+
For universal flow variables, set the anchor id to "".
|
|
412
|
+
:returns: A markdown string representing the table
|
|
413
|
+
"""
|
|
414
|
+
include_units = any(c.units is not None for c in vars)
|
|
415
|
+
if myst_anchor_owner_id is None:
|
|
416
|
+
# Markdown mode
|
|
417
|
+
result = textwrap.dedent(
|
|
418
|
+
f"""
|
|
419
|
+
| Variable Name | Type | Description | Default | {'Units |' * include_units}
|
|
420
|
+
| - | - | - | - | {'- |' * include_units}
|
|
421
|
+
"""
|
|
422
|
+
)
|
|
423
|
+
for var in vars:
|
|
424
|
+
units = var.units or ""
|
|
425
|
+
pdk_superscript = "<sup>PDK</sup>" if var.pdk else ""
|
|
426
|
+
result += f"| `{var.name}` {pdk_superscript} | {var.type_repr_md(for_document=True)} | {var.desc_repr_md()} | `{var.default}` |"
|
|
427
|
+
if include_units:
|
|
428
|
+
result += f" {units} |"
|
|
429
|
+
result += "\n"
|
|
430
|
+
result += "\n"
|
|
431
|
+
else:
|
|
432
|
+
if myst_anchor_owner_id == "":
|
|
433
|
+
# for _get_docs_identifier, where None is the behavior we want
|
|
434
|
+
# for a literal ""
|
|
435
|
+
myst_anchor_owner_id = None
|
|
436
|
+
result = textwrap.dedent(
|
|
437
|
+
f"""
|
|
438
|
+
<div class="table-wrapper colwidths-auto docutils container">
|
|
439
|
+
<table class="docutils align-default">
|
|
440
|
+
<thead><tr>
|
|
441
|
+
<th class="head">Variable Name</th>
|
|
442
|
+
<th class="head">Type</th>
|
|
443
|
+
<th class="head">Description</th>
|
|
444
|
+
<th class="head">Default</th>
|
|
445
|
+
{'<th class="head">Units</th>' * include_units}
|
|
446
|
+
</tr></thead>
|
|
447
|
+
<tbody>
|
|
448
|
+
"""
|
|
449
|
+
)
|
|
450
|
+
for var in vars:
|
|
451
|
+
units = var.units or ""
|
|
452
|
+
pdk_superscript = "<sup>PDK</sup>" if var.pdk else ""
|
|
453
|
+
var_anchor = f"{{#{var._get_docs_identifier(myst_anchor_owner_id)}}}"
|
|
454
|
+
|
|
455
|
+
result += textwrap.dedent(
|
|
456
|
+
f"""
|
|
457
|
+
<tr>
|
|
458
|
+
<td>
|
|
459
|
+
|
|
460
|
+
`{var.name}`{var_anchor} {pdk_superscript}
|
|
461
|
+
"""
|
|
462
|
+
)
|
|
463
|
+
if len(var.deprecated_names):
|
|
464
|
+
result += "<details><summary>Deprecated names</summary>\n\n"
|
|
465
|
+
for dn in var.deprecated_names:
|
|
466
|
+
if isinstance(dn, tuple):
|
|
467
|
+
dn = dn[0]
|
|
468
|
+
result += f"* `{dn}`\n"
|
|
469
|
+
result += "\n</details>\n"
|
|
470
|
+
result += textwrap.dedent(
|
|
471
|
+
f"""
|
|
472
|
+
</td>
|
|
473
|
+
<td>
|
|
474
|
+
|
|
475
|
+
{var.type_repr_md(for_document=True)}
|
|
476
|
+
|
|
477
|
+
</td>
|
|
478
|
+
<td>
|
|
479
|
+
|
|
480
|
+
{var.desc_repr_md()}
|
|
481
|
+
|
|
482
|
+
</td>
|
|
483
|
+
<td>
|
|
484
|
+
|
|
485
|
+
`{var.default}`
|
|
486
|
+
|
|
487
|
+
</td>
|
|
488
|
+
"""
|
|
489
|
+
)
|
|
490
|
+
result += include_units * textwrap.dedent(
|
|
491
|
+
f"""
|
|
492
|
+
<td>
|
|
493
|
+
|
|
494
|
+
{units}
|
|
495
|
+
|
|
496
|
+
</td>
|
|
497
|
+
"""
|
|
498
|
+
)
|
|
499
|
+
result += "\n</tr>"
|
|
500
|
+
result += "</tbody></table></div>\n"
|
|
501
|
+
return result
|
|
502
|
+
|
|
390
503
|
def __process(
|
|
391
504
|
self,
|
|
392
505
|
key_path: str,
|
|
@@ -435,6 +548,9 @@ class Variable:
|
|
|
435
548
|
return_value = list()
|
|
436
549
|
raw = value
|
|
437
550
|
if isinstance(raw, list) or isinstance(raw, tuple):
|
|
551
|
+
if validating_type == List[Path]:
|
|
552
|
+
if any(isinstance(item, List) for item in raw):
|
|
553
|
+
Variable.__flatten_list(value)
|
|
438
554
|
pass
|
|
439
555
|
elif is_string(raw):
|
|
440
556
|
if not permissive_typing:
|
|
@@ -720,3 +836,16 @@ class Variable:
|
|
|
720
836
|
and self.type == rhs.type
|
|
721
837
|
and self.default == rhs.default
|
|
722
838
|
)
|
|
839
|
+
|
|
840
|
+
# Flatten list. Note: Must modify value, not return a new list.
|
|
841
|
+
@staticmethod
|
|
842
|
+
def __flatten_list(value: list):
|
|
843
|
+
new_list = []
|
|
844
|
+
for item in value:
|
|
845
|
+
if isinstance(item, list):
|
|
846
|
+
for sub_item in item:
|
|
847
|
+
new_list.append(sub_item)
|
|
848
|
+
else:
|
|
849
|
+
new_list.append(item)
|
|
850
|
+
|
|
851
|
+
value[:] = new_list
|