Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 1 addition & 11 deletions kicad_lib_manager/commands/add_3d/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,3 @@
import typer

from .command import add_3d

add_3d_app = typer.Typer(
name="add-3d",
help="Add cloud-based 3D model libraries to KiCad configuration",
rich_markup_mode="rich",
callback=add_3d,
invoke_without_command=True,
)

__all__ = ["add_3d", "add_3d_app"]
__all__ = ["add_3d"]
13 changes: 1 addition & 12 deletions kicad_lib_manager/commands/add_hook/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,3 @@
import typer

from .command import add_hook

# Create Typer app and add the add_hook command as callback
add_hook_app = typer.Typer(
name="add-hook",
help="Add a Git post-merge hook to automatically sync KiCad libraries",
rich_markup_mode="rich",
callback=add_hook,
invoke_without_command=True,
)

__all__ = ["add_hook", "add_hook_app"]
__all__ = ["add_hook"]
12 changes: 1 addition & 11 deletions kicad_lib_manager/commands/import_zip/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,3 @@
import typer

from .command import import_zip

import_zip_app = typer.Typer(
name="import",
help="Import SamacSys/Mouser/UltraLibrarian/SnapMagic KiCad ZIP(s) into the configured library",
rich_markup_mode="rich",
callback=import_zip,
invoke_without_command=True,
)

__all__ = ["import_zip", "import_zip_app"]
__all__ = ["import_zip"]
13 changes: 1 addition & 12 deletions kicad_lib_manager/commands/init/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,3 @@
import typer

from .command import init

# Create Typer app and add the init command as callback
init_app = typer.Typer(
name="init",
help="Initialize library configuration",
rich_markup_mode="rich",
callback=init,
invoke_without_command=True,
)

__all__ = ["init", "init_app"]
__all__ = ["init"]
13 changes: 1 addition & 12 deletions kicad_lib_manager/commands/list_libraries/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,3 @@
import typer

from .command import list_cmd

# Create Typer app and add the list command as callback
list_app = typer.Typer(
name="list",
help="List available KiCad libraries",
rich_markup_mode="rich",
callback=list_cmd,
invoke_without_command=True,
)

__all__ = ["list_cmd", "list_app"]
__all__ = ["list_cmd"]
13 changes: 1 addition & 12 deletions kicad_lib_manager/commands/pin/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,3 @@
import typer

from .command import pin

# Create Typer app and add the pin command as callback
pin_app = typer.Typer(
name="pin",
help="Pin favorite libraries",
rich_markup_mode="rich",
callback=pin,
invoke_without_command=True,
)

__all__ = ["pin", "pin_app"]
__all__ = ["pin"]
13 changes: 1 addition & 12 deletions kicad_lib_manager/commands/setup/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,3 @@
import typer

from .command import setup

# Create Typer app and add the setup command as callback
setup_app = typer.Typer(
name="setup",
help="Configure KiCad to use libraries",
rich_markup_mode="rich",
callback=setup,
invoke_without_command=True,
)

__all__ = ["setup", "setup_app"]
__all__ = ["setup"]
13 changes: 1 addition & 12 deletions kicad_lib_manager/commands/status/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,3 @@
import typer

from .command import status

# Create Typer app and add the status command as callback
status_app = typer.Typer(
name="status",
help="Show current library configuration status",
rich_markup_mode="rich",
callback=status,
invoke_without_command=True,
)

__all__ = ["status", "status_app"]
__all__ = ["status"]
13 changes: 1 addition & 12 deletions kicad_lib_manager/commands/sync/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,3 @@
import typer

from .command import sync

# Create Typer app and add the sync command as callback
sync_app = typer.Typer(
name="sync",
help="Update/sync library content from git repositories",
rich_markup_mode="rich",
callback=sync,
invoke_without_command=True,
)

__all__ = ["sync", "sync_app"]
__all__ = ["sync"]
13 changes: 1 addition & 12 deletions kicad_lib_manager/commands/unpin/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,3 @@
import typer

from .command import unpin

# Create Typer app and add the unpin command as callback
unpin_app = typer.Typer(
name="unpin",
help="Unpin libraries in KiCad for quick access",
rich_markup_mode="rich",
callback=unpin,
invoke_without_command=True,
)

__all__ = ["unpin", "unpin_app"]
__all__ = ["unpin"]
51 changes: 26 additions & 25 deletions kicad_lib_manager/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,18 @@
from rich.traceback import install
from typer.core import TyperGroup

from .commands.add_3d import add_3d_app
from .commands.add_hook import add_hook_app
from .commands.add_3d import add_3d
from .commands.add_hook import add_hook
from .commands.config import config_app
from .commands.import_zip import import_zip_app
from .commands.init import init_app
from .commands.list_libraries import list_app
from .commands.pin import pin_app
from .commands.setup import setup_app
from .commands.status import status_app
from .commands.sync import sync_app
from .commands.import_zip import import_zip
from .commands.init import init
from .commands.list_libraries import list_cmd
from .commands.pin import pin
from .commands.setup import setup
from .commands.status import status
from .commands.sync import sync
from .commands.template import template_app
from .commands.unpin import unpin_app
from .commands.unpin import unpin
from .commands.update import update_app
from .services.config_service import ConfigService
from .services.update_service import UpdateService
Expand Down Expand Up @@ -147,24 +147,25 @@ def main(
_check_for_updates_if_needed()


# Register command apps (migrated to Typer)
app.add_typer(status_app, name="status", help="Show current library configuration")
app.add_typer(list_app, name="list", help="List available KiCad libraries")
app.add_typer(init_app, name="init", help="Initialize library configuration")
app.add_typer(pin_app, name="pin", help="Pin favorite libraries")
app.add_typer(unpin_app, name="unpin", help="Unpin favorite libraries")
app.add_typer(setup_app, name="setup", help="Configure KiCad to use libraries")
# Leaf commands are registered as plain commands: a Typer sub-app with only a
# callback renders as a group, and its usage line gets a spurious COMMAND [ARGS]...
app.command("status", help="Show current library configuration")(status)
app.command("list", help="List available KiCad libraries")(list_cmd)
app.command("init", help="Initialize library configuration")(init)
app.command("pin", help="Pin favorite libraries")(pin)
app.command("unpin", help="Unpin favorite libraries")(unpin)
app.command("setup", help="Configure KiCad to use libraries")(setup)
app.command("sync", help="Update/sync library content")(sync)
app.command("add-3d", help="Add 3D model libraries")(add_3d)
app.command("add-hook", help="Add project hooks")(add_hook)
app.command(
"import",
help="Import SamacSys/Mouser/UltraLibrarian/SnapMagic KiCad ZIP(s) into the library",
)(import_zip)
# Groups keep their subcommands.
app.add_typer(config_app, name="config", help="Manage configuration settings")
app.add_typer(template_app, name="template", help="Manage project templates")
app.add_typer(add_3d_app, name="add-3d", help="Add 3D model libraries")
app.add_typer(sync_app, name="sync", help="Update/sync library content")
app.add_typer(update_app, name="update", help="Update KiLM itself")
app.add_typer(add_hook_app, name="add-hook", help="Add project hooks")
app.add_typer(
import_zip_app,
name="import",
help="Import SamacSys/Mouser/UltraLibrarian/SnapMagic KiCad ZIP(s) into the library",
)


if __name__ == "__main__":
Expand Down
51 changes: 51 additions & 0 deletions tests/test_leaf_command_usage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Usage-line contract for the registered top-level commands (issue #13).

A Typer sub-app registered with ``add_typer`` always becomes a click group, so a
leaf command registered that way renders ``COMMAND [ARGS]...`` in its usage line
although it has no subcommands. Leaf commands must use ``app.command``; only real
groups use ``add_typer``. The cases are derived from the app, so a new command
registered the wrong way fails here without editing this file.
"""

from collections.abc import Mapping

import click
import pytest
import typer
from typer.testing import CliRunner

from kicad_lib_manager.main import app

SUBCOMMAND_PLACEHOLDER = "COMMAND [ARGS]..."


def _registered_commands() -> Mapping[str, click.Command]:
root = typer.main.get_command(app)
assert isinstance(root, click.Group)
return root.commands


COMMANDS = _registered_commands()
GROUPS = sorted(name for name, cmd in COMMANDS.items() if isinstance(cmd, click.Group))
LEAVES = sorted(name for name in COMMANDS if name not in GROUPS)


def _usage_line(command: str) -> str:
result = CliRunner().invoke(app, [command, "--help"])
assert result.exit_code == 0, result.output
return next(line for line in result.output.splitlines() if "Usage:" in line)


@pytest.mark.parametrize("command", LEAVES)
def test_leaf_command_usage_has_no_subcommand_placeholder(command: str) -> None:
usage = _usage_line(command)
assert "COMMAND" not in usage
assert "[ARGS]" not in usage


@pytest.mark.parametrize("command", GROUPS)
def test_group_has_subcommands_and_keeps_placeholder(command: str) -> None:
group = COMMANDS[command]
assert isinstance(group, click.Group)
assert group.commands, f"{command} is a group without subcommands; use app.command"
assert SUBCOMMAND_PLACEHOLDER in _usage_line(command)
Loading