refactor: use typer for CLI argument parsing

- Replaces `argparse` with `typer` for command-line argument
  parsing to improve CLI interface and maintainability.
- Converts commands to subcommands using `typer.Typer()`.
- Streamlines CLI structure and enhances user experience.
This commit is contained in:
2025-07-16 11:29:31 +02:00
parent 1d7bc19965
commit 6ca389d5cb
9 changed files with 160 additions and 154 deletions

View File

@@ -19,6 +19,7 @@ pyyaml = "^6.0.2"
pydantic = "^2.11.3" pydantic = "^2.11.3"
rich = "^14.0.0" rich = "^14.0.0"
gitpython = "^3.1.44" gitpython = "^3.1.44"
typer = "^0.16.0"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
twine = "^6.1.0" twine = "^6.1.0"

View File

@@ -1,32 +1,32 @@
import argparse import typer
from importlib.metadata import version, PackageNotFoundError from importlib.metadata import version, PackageNotFoundError
from hdlbuild.commands import register_commands
def get_version(): from hdlbuild.commands.build import cli as build_cli
from hdlbuild.commands.clean import cli as clean_cli
from hdlbuild.commands.dep import cli as dep_cli
from hdlbuild.commands.test import cli as test_cli
from hdlbuild.commands.init import cli as init_cli
def get_version() -> str:
try: try:
return version("hdlbuild") # Paketname aus pyproject.toml return version("hdlbuild")
except PackageNotFoundError: except PackageNotFoundError:
return "unknown" return "unknown"
app = typer.Typer(
rich_help_panel="ℹ️ HDLBuild – FPGA‑Build‑Tool",
help=f"hdlbuild v{get_version()} – Build‑Management for FPGA projects"
)
# Unter‑Kommandos registrieren (entspricht add_subparsers)
app.add_typer(build_cli, name="build", help="Build the project")
app.add_typer(clean_cli, name="clean", help="Clean build artifacts")
app.add_typer(dep_cli, name="dep", help="Resolve dependencies")
app.add_typer(test_cli, name="test", help="Run simulations/testbenches")
app.add_typer(init_cli, name="init", help="Initialize project")
def main(): def main():
version_str = get_version() app()
parser = argparse.ArgumentParser(
description=f"hdlbuild v{version_str} - Build management tool for FPGA projects",
formatter_class=argparse.RawTextHelpFormatter
)
subparsers = parser.add_subparsers(
title="Commands",
description="Available commands",
dest="command",
required=True
)
# Register all commands
register_commands(subparsers)
args = parser.parse_args()
args.func(args)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -1 +0,0 @@
from .commands import register_commands

View File

@@ -1,30 +1,34 @@
from hdlbuild.tools.xilinx_ise.main import xilinx_ise_all, xilinx_ise_synth import typer
from hdlbuild.utils.console_utils import ConsoleUtils
from hdlbuild.utils.directory_manager import ensure_directories_exist
from hdlbuild.utils.project_loader import load_project_config
class BuildCommand: from hdlbuild.tools.xilinx_ise.main import xilinx_ise_all, xilinx_ise_synth
def __init__(self): from hdlbuild.utils.console_utils import ConsoleUtils
self.console_utils = ConsoleUtils("hdlbuild") from hdlbuild.utils.directory_manager import ensure_directories_exist
from hdlbuild.utils.project_loader import load_project_config
def register(self, subparsers): cli = typer.Typer(rich_help_panel="🔨 Build Commands")
parser = subparsers.add_parser("build", help="Start the build process")
parser.add_argument(
"target",
nargs="?",
choices=["synth"],
help="Specify 'synth' to only synthesize the design (optional)"
)
parser.set_defaults(func=self.execute)
def execute(self, args): @cli.callback(invoke_without_command=True)
"""Starts the build process.""" def build(
self.project = load_project_config() target: str = typer.Argument(
if args.target == "synth": None,
self.console_utils.print("Starting synth process...") help="Optional: 'synth' to run synthesis only",
ensure_directories_exist(True) show_default=False,
xilinx_ise_synth(self.project) rich_help_panel="🔨 Build Commands",
else: )
self.console_utils.print("Starting build process...") ) -> None:
ensure_directories_exist(True) """
xilinx_ise_all(self.project) Run the full build flow or synthesis only.
* `hdlbuild build` → full flow
* `hdlbuild build synth` → synthesis only
"""
console = ConsoleUtils("hdlbuild")
project = load_project_config()
ensure_directories_exist(True)
if target == "synth":
console.print("Starting synthesis …")
xilinx_ise_synth(project)
else:
console.print("Starting full build …")
xilinx_ise_all(project)

View File

@@ -1,27 +1,35 @@
from hdlbuild.utils.console_utils import ConsoleUtils import typer
from hdlbuild.utils.directory_manager import clear_build_directories, clear_directories
class CleanCommand: from hdlbuild.utils.console_utils import ConsoleUtils
def __init__(self): from hdlbuild.utils.directory_manager import clear_build_directories, clear_directories
self.console_utils = ConsoleUtils("hdlbuild")
def register(self, subparsers): cli = typer.Typer(rich_help_panel="🧹 Clean Commands")
parser = subparsers.add_parser("clean", help="Clean build artifacts")
parser.add_argument(
"target",
nargs="?",
choices=["all"],
help="Specify 'all' to clean everything (optional)"
)
parser.set_defaults(func=self.execute)
def execute(self, args): @cli.callback(invoke_without_command=True)
"""Cleans the build artifacts.""" def clean(
if args.target == "all": target: str = typer.Argument(
self.console_utils.print("Starting clean all process...") None,
clear_directories() help="Optional: 'all' → wipe *all* artefacts, otherwise only the build directory",
self.console_utils.print("All cleaned.") show_default=False,
else: )
self.console_utils.print("Clearing build artifacts...") ) -> None:
clear_build_directories() """
self.console_utils.print("Build artifacts cleaned.") Remove build artefacts (`build/*`) or *everything* (`all`).
Examples
--------
```bash
hdlbuild clean # build/* and temporary files only
hdlbuild clean all # also caches, logs, etc.
```
"""
console = ConsoleUtils("hdlbuild")
if target == "all":
console.print("Starting clean‑all …")
clear_directories()
console.print("All artefacts removed.")
else:
console.print("Removing build artefacts …")
clear_build_directories()
console.print("Build artefacts removed.")

View File

@@ -1,19 +0,0 @@
from hdlbuild.commands.build import BuildCommand
from hdlbuild.commands.clean import CleanCommand
from hdlbuild.commands.dep import DepCommand
from hdlbuild.commands.init import InitCommand
from hdlbuild.commands.test import TestCommand
def register_commands(subparsers):
"""Registers all available commands."""
commands = [
CleanCommand(),
BuildCommand(),
DepCommand(),
TestCommand(),
InitCommand(),
]
for command in commands:
command.register(subparsers)

View File

@@ -1,17 +1,23 @@
from hdlbuild.dependencies.resolver import DependencyResolver import typer
from hdlbuild.utils.console_utils import ConsoleUtils
from hdlbuild.utils.project_loader import load_project_config
class DepCommand: from hdlbuild.dependencies.resolver import DependencyResolver
def __init__(self): from hdlbuild.utils.console_utils import ConsoleUtils
self.console_utils = ConsoleUtils("hdlbuild") from hdlbuild.utils.project_loader import load_project_config
def register(self, subparsers): cli = typer.Typer(rich_help_panel="🔗 Dependency Commands")
parser = subparsers.add_parser("dep", help="Start the dependencies process")
parser.set_defaults(func=self.execute)
def execute(self, args): @cli.callback(invoke_without_command=True)
"""Starts the dependencies process.""" def dep() -> None:
self.project = load_project_config() """
self.console_utils.print("Starting dependencies process...") Resolve all project dependencies.
DependencyResolver(self.project).resolve_all()
```bash
hdlbuild dep
```
"""
console = ConsoleUtils("hdlbuild")
project = load_project_config()
console.print("Resolving dependencies …")
DependencyResolver(project).resolve_all()
console.print("Dependencies resolved.")

View File

@@ -1,36 +1,35 @@
from pathlib import Path
import shutil import shutil
from hdlbuild.dependencies.resolver import DependencyResolver from pathlib import Path
import typer
from hdlbuild.utils.console_utils import ConsoleUtils from hdlbuild.utils.console_utils import ConsoleUtils
class InitCommand: cli = typer.Typer(rich_help_panel="🆕 Init Commands")
def __init__(self):
self.console_utils = ConsoleUtils("hdlbuild")
def register(self, subparsers): @cli.callback(invoke_without_command=True)
parser = subparsers.add_parser("init", help="Initialize a new HDLBuild project") def init() -> None:
parser.set_defaults(func=self.execute) """
Initialise a new HDLBuild project in the current directory.
def execute(self, args): Copies `.gitignore` and `project.yml` from the template folder.
"""Initialize a new HDLBuild project.""" """
project_dir = Path.cwd() console = ConsoleUtils("hdlbuild")
project_dir = Path.cwd()
# Correctly resolve path to templates directory script_dir = Path(__file__).parent.resolve()
script_dir = Path(__file__).parent.resolve() template_dir = (script_dir / ".." / "templates").resolve()
template_dir = (script_dir / ".." / "templates").resolve()
# Files to copy files = [
files = [ ("gitignore.template", ".gitignore"),
("gitignore.template", ".gitignore"), ("project.yml.template", "project.yml"),
("project.yml.template", "project.yml"), ]
]
for template_name, target_name in files: for template_name, target_name in files:
template_path = template_dir / template_name template_path = template_dir / template_name
target_path = project_dir / target_name target_path = project_dir / target_name
if not target_path.exists(): if not target_path.exists():
shutil.copy(template_path, target_path) shutil.copy(template_path, target_path)
self.console_utils.print(f"Created {target_name}") console.print(f"Created {target_name}")
else: else:
self.console_utils.print(f"{target_name} already exists, skipping.") console.print(f"{target_name} already exists skipping.")

View File

@@ -1,23 +1,31 @@
from hdlbuild.tools.xilinx_ise.isim import build_testbench, run_testbench import typer
from hdlbuild.utils.console_utils import ConsoleUtils
from hdlbuild.utils.project_loader import load_project_config
class TestCommand: from hdlbuild.tools.xilinx_ise.isim import build_testbench, run_testbench
def __init__(self): from hdlbuild.utils.console_utils import ConsoleUtils
self.console_utils = ConsoleUtils("hdlbuild") from hdlbuild.utils.project_loader import load_project_config
def register(self, subparsers): cli = typer.Typer(rich_help_panel="🧪 Test Commands")
parser = subparsers.add_parser("test", help="Start the Tests process")
parser.add_argument(
"target",
nargs="?",
help="Select the target to test"
)
parser.set_defaults(func=self.execute)
def execute(self, args): @cli.callback(invoke_without_command=True)
"""Starts the test process.""" def test(
self.project = load_project_config() target: str = typer.Argument(
self.console_utils.print("Starting test process...") None,
build_testbench(self.project, args.target) help="Name of the test target (leave empty to run all)",
run_testbench(self.project, args.target) show_default=False,
)
) -> None:
"""
Build and run testbenches.
```bash
hdlbuild test # run all TBs
hdlbuild test alu # run TB 'alu' only
```
"""
console = ConsoleUtils("hdlbuild")
project = load_project_config()
console.print("Starting test flow …")
build_testbench(project, target)
run_testbench(project, target)
console.print("Tests finished.")