• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nbiotcloud / ucdp / 14203596913

01 Apr 2025 06:46PM UTC coverage: 96.794% (-0.04%) from 96.833%
14203596913

push

github

iccode17
improve error message

0 of 2 new or added lines in 1 file covered. (0.0%)

1 existing line in 1 file now uncovered.

4770 of 4928 relevant lines covered (96.79%)

7.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.47
/src/ucdp/cli.py
1
#
2
# MIT License
3
#
4
# Copyright (c) 2024-2025 nbiotcloud
5
#
6
# Permission is hereby granted, free of charge, to any person obtaining a copy
7
# of this software and associated documentation files (the "Software"), to deal
8
# in the Software without restriction, including without limitation the rights
9
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
# copies of the Software, and to permit persons to whom the Software is
11
# furnished to do so, subject to the following conditions:
12
#
13
# The above copyright notice and this permission notice shall be included in all
14
# copies or substantial portions of the Software.
15
#
16
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
# SOFTWARE.
23
#
24

25
"""Command Line Interface."""
26

27
import logging
8✔
28
import sys
8✔
29
from collections import defaultdict
8✔
30
from collections.abc import Iterable
8✔
31
from logging import StreamHandler
8✔
32
from pathlib import Path
8✔
33

34
import click
8✔
35
from click_bash42_completion import patch
8✔
36
from pydantic import BaseModel, ConfigDict
8✔
37
from rich.console import Console
8✔
38
from rich.logging import RichHandler
8✔
39
from rich.pretty import pprint
8✔
40
from rich.table import Table
8✔
41

42
from ._cligroup import MainGroup
8✔
43
from ._logging import HasErrorHandler
8✔
44
from .cache import CACHE
8✔
45
from .cliutil import (
8✔
46
    PathType,
47
    arg_template_filepaths,
48
    arg_top,
49
    arg_tops,
50
    auto_path,
51
    defines2data,
52
    opt_check,
53
    opt_defines,
54
    opt_dry_run,
55
    opt_file,
56
    opt_filelist,
57
    opt_filepath,
58
    opt_local,
59
    opt_maxlevel,
60
    opt_maxworkers,
61
    opt_path,
62
    opt_show_diff,
63
    opt_tag,
64
    opt_target,
65
    opt_topsfile,
66
    read_file,
67
)
68
from .consts import PATH
8✔
69
from .fileset import FileSet
8✔
70
from .finder import find
8✔
71
from .generate import Generator, clean, get_makolator, render_generate, render_inplace
8✔
72
from .iterutil import namefilter
8✔
73
from .loader import load
8✔
74
from .modfilelist import iter_modfilelists
8✔
75
from .modtopref import PAT_TOPMODREF, TopModRef
8✔
76
from .pathutil import relative
8✔
77
from .top import Top
8✔
78
from .util import LOGGER, guess_path
8✔
79

80
patch()
8✔
81

82

83
_LOGLEVELMAP = {
8✔
84
    0: logging.WARNING,
85
    1: logging.INFO,
86
    2: logging.DEBUG,
87
}
88

89

90
class Ctx(BaseModel):
8✔
91
    """Command Line Context."""
92

93
    model_config = ConfigDict(
8✔
94
        arbitrary_types_allowed=True,
95
    )
96

97
    console: Console
8✔
98
    has_error_handler: HasErrorHandler | None = None
8✔
99

100
    verbose: int = 0
8✔
101
    no_cache: bool = False
8✔
102
    no_color: bool | None = None
8✔
103

104
    @staticmethod
8✔
105
    def create(no_color: bool | None = None, **kwargs) -> "Ctx":
8✔
106
        """Create."""
107
        console = Console(log_time=False, log_path=False, no_color=no_color)
8✔
108
        has_error_handler = HasErrorHandler()
8✔
109
        return Ctx(console=console, has_error_handler=has_error_handler, no_color=no_color, **kwargs)
8✔
110

111
    def __enter__(self):
8✔
112
        # Logging
113
        level = _LOGLEVELMAP.get(self.verbose, logging.DEBUG)
8✔
114
        if not self.no_color:
8✔
115
            handler = RichHandler(
×
116
                show_time=False,
117
                show_path=False,
118
                rich_tracebacks=True,
119
                console=Console(stderr=True, no_color=self.no_color),
120
            )
121
            format_ = "%(message)s"
×
122
        else:
123
            handler = StreamHandler(stream=sys.stderr)
8✔
124
            format_ = "%(levelname)s %(message)s"
8✔
125
        handlers = [handler, self.has_error_handler]
8✔
126
        logging.basicConfig(level=level, format=format_, handlers=handlers)
8✔
127

128
        # Cache
129
        if self.no_cache:
8✔
130
            CACHE.disable()
×
131

132
        return self
8✔
133

134
    def __exit__(self, exc_type, exc_value, tb):
8✔
135
        if exc_type or self.has_error_handler.has_errors:
8✔
136
            self.console.print("[red][bold]Aborted.")
8✔
137
            sys.exit(1)
8✔
138

139

140
@click.group(cls=MainGroup, context_settings={"help_option_names": ["-h", "--help"]})
8✔
141
@click.option("-v", "--verbose", count=True, help="Increase Verbosity.")
8✔
142
@click.option("-C", "--no-cache", is_flag=True, help="Disable Caching.")
8✔
143
@click.option("--no-color", is_flag=True, help="Disable Coloring.", envvar="UCDP_NO_COLOR")
8✔
144
@click.version_option()
8✔
145
@click.pass_context
8✔
146
def ucdp(ctx, verbose=0, no_cache=False, no_color=False):
8✔
147
    """Unified Chip Design Platform."""
148
    ctx.obj = ctx.with_resource(Ctx.create(verbose=verbose, no_cache=no_cache, no_color=no_color))
8✔
149

150

151
pass_ctx = click.make_pass_decorator(Ctx)
8✔
152

153

154
def get_group(help=None):  # pragma: no cover
155
    """Create Command Group."""
156

157
    @click.group(help=help)
158
    @click.pass_context
159
    def group(ctx):
160
        ctx.obj = Ctx(console=Console(log_time=False, log_path=False))
161

162
    return group
163

164

165
def load_top(ctx: Ctx, top: str | TopModRef, paths: Iterable[str | Path], quiet: bool = False) -> Top:
8✔
166
    """Load Top Module."""
167
    lpaths = [Path(path) for path in paths]
8✔
168
    # Check if top seems to be some kind of file path
169
    topmodref = TopModRef.cast(guess_path(top) or top) if isinstance(top, str) else top
8✔
170
    if quiet:
8✔
171
        return load(topmodref, paths=lpaths)
8✔
172
    with ctx.console.status(f"Loading '{topmodref!s}'"):
8✔
173
        result = load(topmodref, paths=lpaths)
8✔
174
    ctx.console.log(f"'{topmodref!s}' checked.")
8✔
175
    return result
8✔
176

177

178
@ucdp.command(
8✔
179
    help=f"""
180
Load Data Model and Check.
181

182
TOP: Top Module. {PAT_TOPMODREF}. Environment Variable 'UCDP_TOP'
183
"""
184
)
185
@arg_top
8✔
186
@opt_path
8✔
187
@click.option("--stat", default=False, is_flag=True, help="Show Statistics.")
8✔
188
@pass_ctx
8✔
189
def check(ctx, top, path, stat=False):
8✔
190
    """Check."""
191
    top = load_top(ctx, top, path)
8✔
192
    if stat:
8✔
193
        print("Statistics:")
8✔
194
        for name, value in top.get_stat().items():
8✔
195
            print(f"  {name}: {value}")
8✔
196

197

198
@ucdp.command(
8✔
199
    help=f"""
200
Load Data Model and Generate Files.
201

202
TOP: Top Module. {PAT_TOPMODREF}. Environment Variable 'UCDP_TOP'
203
"""
204
)
205
@arg_tops
8✔
206
@opt_path
8✔
207
@opt_filelist
8✔
208
@opt_target
8✔
209
@opt_show_diff
8✔
210
@opt_maxworkers
8✔
211
@opt_defines
8✔
212
@opt_local
8✔
213
@opt_topsfile
8✔
214
@opt_check
8✔
215
@pass_ctx
8✔
216
def gen(
8✔
217
    ctx,
218
    tops,
219
    path,
220
    filelist,
221
    target=None,
222
    show_diff=False,
223
    maxworkers=None,
224
    define=None,
225
    local=None,
226
    check=False,
227
    tops_file=None,
228
):
229
    """Generate."""
230
    tops = list(tops)
8✔
231
    for filepath in tops_file or []:
8✔
232
        tops.extend(read_file(filepath))
8✔
233
    makolator = get_makolator(show_diff=show_diff, paths=path)
8✔
234
    data = defines2data(define)
8✔
235
    filelist = filelist or ["*"]
8✔
236
    with Generator(makolator=makolator, maxworkers=maxworkers, check=check) as generator:
8✔
237
        for info in find(path, patterns=tuple(tops), local=local, is_top=True):
8✔
238
            try:
8✔
239
                top = load_top(ctx, info.topmodref, path)
8✔
240
            except Exception as exc:
×
NEW
241
                LOGGER.warning(f"Cannot load '{info.topmodref}'")
×
242
                LOGGER.warning(str(exc))
×
NEW
243
                LOGGER.warning(f"Debug with 'ucdp check {info.topmodref}'")
×
UNCOV
244
                continue
×
245
            for item in filelist:
8✔
246
                generator.generate(top, item, target=target, data=data)
8✔
247

248

249
@ucdp.command(
8✔
250
    help=f"""
251
Load Data Model and Render Template and Create File.
252

253
TOP: Top Module. {PAT_TOPMODREF}. Environment Variable 'UCDP_TOP'
254

255
TEMPLATE_FILEPATHS: Templates to render. Environment Variable 'UCDP_TEMPLATE_FILEPATHS'
256
                    Templates in `templates` folders are found automatically.
257

258
GENFILE: Generated File.
259
"""
260
)
261
@arg_top
8✔
262
@opt_path
8✔
263
@arg_template_filepaths
8✔
264
@click.argument("genfile", type=PathType, shell_complete=auto_path, nargs=1)
8✔
265
@opt_show_diff
8✔
266
@opt_defines
8✔
267
@pass_ctx
8✔
268
def rendergen(ctx, top, path, template_filepaths, genfile, show_diff=False, define=None):
8✔
269
    """Render Generate."""
270
    top = load_top(ctx, top, path)
8✔
271
    makolator = get_makolator(show_diff=show_diff, paths=path)
8✔
272
    data = defines2data(define)
8✔
273
    render_generate(top, template_filepaths, genfile=genfile, makolator=makolator, data=data)
8✔
274

275

276
@ucdp.command(
8✔
277
    help=f"""
278
Load Data Model and Render Template and Update File.
279

280
TOP: Top Module. {PAT_TOPMODREF}. Environment Variable 'UCDP_TOP'
281

282
TEMPLATE_FILEPATHS: Templates to render. Environment Variable 'UCDP_TEMPLATE_FILEPATHS'
283
                    Templates in `templates` folders are found automatically.
284

285
INPLACEFILE: Inplace File.
286
"""
287
)
288
@arg_top
8✔
289
@opt_path
8✔
290
@arg_template_filepaths
8✔
291
@click.argument("inplacefile", type=PathType, shell_complete=auto_path, nargs=1)
8✔
292
@opt_show_diff
8✔
293
@opt_defines
8✔
294
@click.option("--ignore_unknown", "-i", default=False, is_flag=True, help="Ignore Unknown Placeholder.")
8✔
295
@pass_ctx
8✔
296
def renderinplace(ctx, top, path, template_filepaths, inplacefile, show_diff=False, define=None, ignore_unknown=False):
8✔
297
    """Render Inplace."""
298
    top = load_top(ctx, top, path)
8✔
299
    makolator = get_makolator(show_diff=show_diff, paths=path)
8✔
300
    data = defines2data(define)
8✔
301
    render_inplace(
8✔
302
        top,
303
        template_filepaths,
304
        inplacefile=inplacefile,
305
        makolator=makolator,
306
        data=data,
307
        ignore_unknown=ignore_unknown,
308
    )
309

310

311
@ucdp.command(
8✔
312
    help=f"""
313
Load Data Model and REMOVE Generated Files.
314

315
TOP: Top Module. {PAT_TOPMODREF}. Environment Variable 'UCDP_TOP'
316
"""
317
)
318
@arg_top
8✔
319
@opt_path
8✔
320
@opt_filelist
8✔
321
@opt_target
8✔
322
@opt_show_diff
8✔
323
@opt_dry_run
8✔
324
@opt_maxworkers
8✔
325
@pass_ctx
8✔
326
def cleangen(ctx, top, path, filelist, target=None, show_diff=False, maxworkers=None, dry_run=False):
8✔
327
    """Clean Generated Files."""
328
    top = load_top(ctx, top, path)
8✔
329
    makolator = get_makolator(show_diff=show_diff, paths=path)
8✔
330
    for item in filelist or ["*"]:
8✔
331
        clean(top, item, target=target, makolator=makolator, maxworkers=maxworkers, dry_run=dry_run)
8✔
332

333

334
@ucdp.command(
8✔
335
    help=f"""
336
Load Data Model and Generate File List.
337

338
TOP: Top Module. {PAT_TOPMODREF}. Environment Variable 'UCDP_TOP'
339
"""
340
)
341
@arg_top
8✔
342
@opt_path
8✔
343
@opt_filelist
8✔
344
@opt_target
8✔
345
@opt_file
8✔
346
@pass_ctx
8✔
347
def filelist(ctx, top, path, filelist, target=None, file=None):
8✔
348
    """File List."""
349
    # Load quiet, otherwise stdout is messed-up
350
    top = load_top(ctx, top, path, quiet=True)
8✔
351
    for item in filelist or ["*"]:
8✔
352
        fileset = FileSet.from_mod(top.mod, item, target=target)
8✔
353
        for line in fileset:
8✔
354
            print(line, file=file)
8✔
355

356

357
@ucdp.command(
8✔
358
    help=f"""
359
Load Data Model and Show File Information
360

361
TOP: Top Module. {PAT_TOPMODREF}. Environment Variable 'UCDP_TOP'
362
"""
363
)
364
@arg_top
8✔
365
@opt_path
8✔
366
@opt_filelist
8✔
367
@opt_target
8✔
368
@opt_maxlevel
8✔
369
@click.option("--minimal", "-m", default=False, is_flag=True, help="Skip defaults.")
8✔
370
@opt_file
8✔
371
@pass_ctx
8✔
372
def fileinfo(ctx, top, path, filelist, target=None, maxlevel=None, minimal=False, file=None):
8✔
373
    """File List."""
374
    # Load quiet, otherwise stdout is messed-up
375
    top = load_top(ctx, top, path, quiet=True)
8✔
376
    console = Console(file=file) if file else ctx.console
8✔
377
    for item in filelist or ["*"]:
8✔
378
        data = defaultdict(list)
8✔
379
        for mod, modfilelist in iter_modfilelists(top.mod, item, target=target, maxlevel=maxlevel):
8✔
380
            data[str(mod)].append(modfilelist.model_dump(exclude_defaults=minimal))
8✔
381
        pprint(dict(data), indent_guides=False, console=console)
8✔
382

383

384
@ucdp.command(
8✔
385
    help="""
386
              List Available Data Models.
387

388
              PATTERN: Limit list to these modules only.
389

390
              Examples:
391

392
                ucdp ls
393

394
                ucdp ls -n
395

396
                ucdp ls glbl_lib*
397
              """
398
)
399
@arg_tops
8✔
400
@opt_path
8✔
401
@click.option("--names", "-n", default=False, is_flag=True, help="Just print names")
8✔
402
@click.option("--top/--no-top", "-t/-T", default=None, is_flag=True, help="List loadable top modules only.")
8✔
403
@click.option("--tb/--no-tb", "-b/-B", default=None, is_flag=True, help="List testbench modules only.")
8✔
404
@click.option("--generic-tb", "-g", default=False, is_flag=True, help="List Generic Testbench modules only.")
8✔
405
@opt_local
8✔
406
@click.option("--base", "-A", default=False, is_flag=True, help="Show Base Classes.")
8✔
407
@click.option("--filepath", "-f", default=False, is_flag=True, help="Show File Path.")
8✔
408
@click.option("--abs-filepath", "-F", default=False, is_flag=True, help="Show Absolute File Path.")
8✔
409
@opt_tag
8✔
410
@pass_ctx
8✔
411
def ls(  # noqa: C901
8✔
412
    ctx,
413
    path=None,
414
    tops=None,
415
    names=False,
416
    top=None,
417
    tb=None,
418
    local=None,
419
    generic_tb=False,
420
    tag=None,
421
    base=False,
422
    filepath=False,
423
    abs_filepath=False,
424
):
425
    """List Modules."""
426
    with ctx.console.status("Searching"):
8✔
427
        infos = find(path, patterns=tops or ["*"], local=local)
8✔
428
    if top is not None:
8✔
429
        infos = [info for info in infos if info.is_top == top]
8✔
430
    if tb is not None:
8✔
431
        infos = [info for info in infos if bool(info.tb) == tb]
8✔
432
    if generic_tb:
8✔
433
        infos = [info for info in infos if info.tb == "Generic"]
8✔
434
    if tag:
8✔
435
        filter_ = namefilter(tag)
8✔
436
        infos = [info for info in infos if any(filter_(tag) for tag in info.tags)]
8✔
437

438
    def fill_row(row, info):
8✔
439
        if base:
8✔
440
            row.append(info.modbasecls.__name__)
8✔
441
        if filepath:
8✔
442
            row.append(str(relative(info.filepath)))
8✔
443
        if abs_filepath:
8✔
444
            row.append(str(info.filepath))
8✔
445

446
    if names:
8✔
447
        for info in infos:
8✔
448
            row = [info.topmodref]
8✔
449
            fill_row(row, info)
8✔
450
            print(*row)
8✔
451
    else:
452
        table = Table(expand=filepath or abs_filepath)
8✔
453
        table.add_column("Reference")
8✔
454
        table.add_column("Top", justify="center")
8✔
455
        table.add_column("Tb ", justify="center")
8✔
456
        table.add_column("Tags")
8✔
457
        if base:
8✔
458
            table.add_column("Bases on", justify="right")
8✔
459
        if filepath:
8✔
460
            table.add_column("Filepath")
×
461
        if abs_filepath:
8✔
462
            table.add_column("Absolute Filepath")
×
463
        for info in infos:
8✔
464
            row = [
8✔
465
                str(info.topmodref),
466
                "X" if info.is_top else "",
467
                "X" if info.tb else "",
468
                ",".join(sorted(info.tags)),
469
            ]
470
            fill_row(row, info)
8✔
471
            table.add_row(*row)
8✔
472
        ctx.console.print(table)
8✔
473

474

475
@ucdp.command(
8✔
476
    help=f"""
477
Load Data Model and Module Information.
478

479
TOP: Top Module. {PAT_TOPMODREF}. Environment Variable 'UCDP_TOP'
480
"""
481
)
482
@arg_tops
8✔
483
@opt_path
8✔
484
@opt_local
8✔
485
@click.option("--top", "-t", default=None, is_flag=True, help="List loadable top modules only.")
8✔
486
@click.option("--sub", "-S", default=False, is_flag=True, help="Show Submodules.")
8✔
487
@pass_ctx
8✔
488
def modinfo(ctx, tops, path, local, top, sub):
8✔
489
    """Module Information."""
490
    sep = ""
8✔
491
    for info in find(path, patterns=tops, local=local, is_top=top):
8✔
492
        try:
8✔
493
            top = load_top(ctx, info.topmodref, path, quiet=True)
8✔
494
        except Exception as exc:
8✔
495
            LOGGER.warning(str(exc))
8✔
496
            continue
8✔
497
        print(sep + top.mod.get_info(sub=sub))
8✔
498
        sep = "\n\n"
8✔
499

500

501
@ucdp.command(
8✔
502
    help=f"""
503
Load Data Model and Show Module Overview.
504

505
TOP: Top Module. {PAT_TOPMODREF}. Environment Variable 'UCDP_TOP'
506
"""
507
)
508
@arg_top
8✔
509
@opt_path
8✔
510
@click.option("--minimal", "-m", default=False, is_flag=True, help="Skip modules without specific details")
8✔
511
@opt_filepath
8✔
512
@opt_tag
8✔
513
@pass_ctx
8✔
514
def overview(ctx, top, path, minimal=False, file=None, tag=None):
8✔
515
    """Overview."""
516
    # Load quiet, otherwise stdout is messed-up
517
    top = load_top(ctx, top, path, quiet=True)
8✔
518
    data = {"minimal": minimal, "tags": tag}
8✔
519
    render_generate(top, [PATH / "ucdp-templates" / "overview.txt.mako"], genfile=file, data=data, no_stat=True)
8✔
520

521

522
@ucdp.group(context_settings={"help_option_names": ["-h", "--help"]})
8✔
523
def info():
8✔
524
    """Information."""
525

526

527
@info.command()
8✔
528
@pass_ctx
8✔
529
def examples(ctx):
8✔
530
    """Path to Examples."""
531
    examples_path = Path(__file__).parent / "examples"
8✔
532
    print(str(examples_path))
8✔
533

534

535
@info.command()
8✔
536
@opt_path
8✔
537
@pass_ctx
8✔
538
def template_paths(ctx, path):
8✔
539
    """Template Paths."""
540
    makolator = get_makolator(paths=path)
8✔
541
    for template_path in makolator.config.template_paths:
8✔
542
        print(str(template_path))
8✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc