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
13 changes: 8 additions & 5 deletions kicad_lib_manager/commands/template/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ def create( # noqa: PLR0913 -- Typer maps each CLI option to one parameter
directory), named by the ``directory_name`` variable or derived from the
project name. A NAME with a path separator gives both the parent directory
and the name. Variables not set by NAME or ``--set-var`` are prompted for.
Exits with status 1 when no template can be created or selected, or when
project creation fails.
"""
templates = find_all_templates(Config(), library)
if not templates:
Expand All @@ -74,23 +76,23 @@ def create( # noqa: PLR0913 -- Typer maps each CLI option to one parameter
)
get_console().print(f"No templates found in {where}.")
get_console().print("Use 'kilm template make' to create a template first.")
return
raise typer.Exit(1)

name, base_directory = _split_name_and_directory(name, directory)
selected = _select_template(templates, template)
if selected is None:
return
raise typer.Exit(1)
variables = _collect_variables(selected, name, set_var or [])
if variables is None:
return
raise typer.Exit(1)
get_console().print()
get_console().print(f"Final variables: {json.dumps(variables, indent=2)}")

project_dir = _target_directory(
base_directory, selected, variables, dry_run=dry_run
)
if project_dir is None:
return
raise typer.Exit(1)
if os.name == "nt":
_warn_old_filename_syntax(Path(selected["path"]) / TEMPLATE_CONTENT_DIR)
try:
Expand All @@ -103,7 +105,8 @@ def create( # noqa: PLR0913 -- Typer maps each CLI option to one parameter
if not success:
get_console().print()
get_console(stderr=True).print("Project creation failed.")
elif not dry_run:
raise typer.Exit(1)
if not dry_run:
get_console().print()
get_console().print(
f"Project '{variables.get(PROJECT_NAME, name)}' created successfully"
Expand Down
6 changes: 5 additions & 1 deletion kicad_lib_manager/commands/template/docs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ kilm template make [template_name] [project_path] [OPTIONS]
Overwrite the template directory if it already exists. Default: `False`.
- `--help`: Show help message.

`kilm template make` exits with status `1` when no GitHub library is configured, when no library path is known in `--non-interactive` mode, or when the template already exists and `--force` is not given.

#### Behavior

1. **Scans Project:** Analyzes the source project directory.
Expand Down Expand Up @@ -96,6 +98,8 @@ kilm template create [project_name_or_path] [output_path] [OPTIONS]
- `--dry-run`: Preview project creation without creating files.
- `--help`: Show help message.

`kilm template create` exits with status `1` when no template can be created: no templates are configured, the requested template is not found, a prompt is cancelled, the project directory cannot be determined, or project creation fails.

#### Behavior

1. **Finds Template:** Locates the specified (or selected) template within the configured KiLM libraries.
Expand Down Expand Up @@ -148,7 +152,7 @@ kilm template list [OPTIONS]

- `--library TEXT`: List templates only from the specified KiLM library name.
- `-v, --verbose`: Add the template variables and the parent template (`extends`) to the table.
- `--json`: Output the list in JSON format.
- `--json`: Output the list in JSON format. In JSON mode stdout carries only JSON: an empty library set prints `{}`, and the update notice, when one is due, goes to stderr.
- `--help`: Show help message.

#### Examples
Expand Down
9 changes: 5 additions & 4 deletions kicad_lib_manager/commands/template/listing.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,15 @@ def list_templates(
``--json`` prints the discovered templates as JSON on stdout instead.
"""
templates = find_all_templates(Config(), library)
if not templates:
_print_no_templates(library)
return
if json_output:
# Plain echo: a Rich console would wrap long values and break the JSON.
# Custom keys may hold YAML dates and such, which print as text.
# Custom keys may hold YAML dates and such, which print as text. An
# empty library set prints `{}`, keeping stdout pure JSON.
typer.echo(json.dumps(_json_keys(templates), indent=2, default=str))
return
if not templates:
_print_no_templates(library)
return

by_library: dict[str, list[tuple[str, DiscoveredTemplate]]] = {}
for name, data in templates.items():
Expand Down
8 changes: 5 additions & 3 deletions kicad_lib_manager/commands/template/make.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,9 @@ def make( # noqa: PLR0913 -- Typer maps each CLI option to one parameter
use case, target library) and offers values found in the project as
variables. With ``--non-interactive``, the name defaults to the source
directory's and the output to ``templates/<name>`` in the symbol library.
Exits with status 1 when no GitHub library is configured.
Exits with status 1 when no GitHub library is configured, when no
library path is known in ``--non-interactive`` mode, or when the template
already exists and ``--force`` is not given.
"""
config = Config()
libraries = config.get_libraries(library_type=GITHUB_LIBRARY)
Expand Down Expand Up @@ -132,13 +134,13 @@ def make( # noqa: PLR0913 -- Typer maps each CLI option to one parameter
has_output=output_directory is not None,
)
if target is None:
return
raise typer.Exit(1)
if target.output.exists() and not force:
get_console().print(
f"Template '{target.name}' already exists at {target.output}"
)
get_console().print("Use --force to overwrite.")
return
raise typer.Exit(1)

excludes = exclude or []
_print_request(target, excludes, extends)
Expand Down
4 changes: 2 additions & 2 deletions kicad_lib_manager/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,10 @@ def _check_for_updates_if_needed() -> None:
return
update_service = UpdateService(importlib.metadata.version("kilm"))
if config.should_check_updates():
if update_service.show_update_notification(quiet=True):
if update_service.show_update_notification(quiet=True, stderr=True):
config.mark_update_check_performed()
elif update_service.check_for_updates(use_cache=True)["has_update"]:
update_service.show_update_notification(quiet=True)
update_service.show_update_notification(quiet=True, stderr=True)
except Exception:
logger.debug("Update check failed", exc_info=True)

Expand Down
6 changes: 4 additions & 2 deletions kicad_lib_manager/services/update_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,17 @@ def check_for_updates(self, *, use_cache: bool = True) -> UpdateStatus:
}

def show_update_notification(
self, *, quiet: bool = False, force_check: bool = False
self, *, quiet: bool = False, force_check: bool = False, stderr: bool = False
) -> bool:
"""Print a panel when a newer release exists; return whether one does.

Args:
quiet: Print nothing when KiLM is up to date.
force_check: Ask PyPI even when a recent answer is cached.
stderr: Write the notice to stderr, for commands whose stdout
output must stay pure, such as ``--json`` modes.
"""
console = get_console()
console = get_console(stderr=stderr)
status = self.check_for_updates(use_cache=not force_check)
if not status["has_update"]:
if not quiet:
Expand Down
6 changes: 3 additions & 3 deletions kicad_lib_manager/utils/template/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ def post_create(context):
# "Created with KiCad Library Manager\n"
# )

# Print message to user
print(f"Project {context['variables']['project_name']} created successfully!")
print(f"Location: {context['project_dir']}")
# Example: Print a message to the user
# print(f"Project {context['variables']['project_name']} created successfully!")
# print(f"Location: {context['project_dir']}")
'''


Expand Down
9 changes: 7 additions & 2 deletions tests/test_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,12 @@ def test_create_project_dry_run_writes_nothing(


def test_starting_hook_runs(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
"""The hook that template make writes loads and runs as written."""
"""The hook that template make writes loads and runs as written.

`template create` prints its own success message with the project
location, so the written hook stays silent (it used to duplicate the
message).
"""
create_hook_script(tmp_path)

run_post_create_hook(
Expand All @@ -267,7 +272,7 @@ def test_starting_hook_runs(tmp_path: Path, capsys: pytest.CaptureFixture[str])
create_template_metadata("tpl"),
)

assert "Project MyProject created successfully!" in capsys.readouterr().out
assert capsys.readouterr().out == ""


class _LibraryConfig:
Expand Down
68 changes: 68 additions & 0 deletions tests/test_template_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,74 @@ def test_list_json_is_valid_json_for_long_values(home: Path) -> None:
assert templates["board"]["description"] == "x" * 150


@pytest.mark.usefixtures("home")
def test_list_json_with_no_templates_prints_an_empty_object() -> None:
"""--json answers an empty library set with `{}`, not rich text."""
result = CliRunner().invoke(template_app, ["list", "--json"])

assert result.exit_code == 0
assert json.loads(result.output) == {}


@pytest.mark.usefixtures("home")
def test_create_without_templates_exits_nonzero() -> None:
"""`template create` fails when no library has any templates."""
result = CliRunner().invoke(template_app, ["create", "Widget"])

assert result.exit_code == 1
assert "No templates found" in result.output


def test_create_unknown_template_exits_nonzero(home: Path) -> None:
"""`template create` fails when the requested template does not exist."""
add_template(add_library(home, "lib"), "board", {})

result = CliRunner().invoke(
template_app, ["create", "Widget", "--template", "missing"]
)

assert result.exit_code == 1
assert "Template 'missing' not found." in result.output


def test_create_cancelled_selection_exits_nonzero(
home: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""A cancelled template selection fails the command."""
add_template(add_library(home, "lib"), "board", {})

class Cancelled:
def __init__(
self, message: str, choices: object = None, **kwargs: object
) -> None:
pass

def ask(self) -> None:
return None

monkeypatch.setattr(questionary, "select", Cancelled)

result = CliRunner().invoke(template_app, ["create", "Widget"])

assert result.exit_code == 1
assert "Template selection cancelled." in result.output


def test_make_existing_template_exits_nonzero(home: Path, tmp_path: Path) -> None:
"""`template make` refuses to overwrite without --force."""
library = add_library(home, "lib")
(library / TEMPLATES_DIR / "source").mkdir(parents=True)
source = tmp_path / "source"
source.mkdir()

result = CliRunner().invoke(
template_app, ["make", "source", str(source), "--non-interactive"]
)

assert result.exit_code == 1
assert "already exists" in result.output


def test_list_json_keeps_custom_metadata(home: Path) -> None:
"""Keys KiLM does not know, YAML dates included, appear in the JSON."""
template_dir = add_template(add_library(home, "lib"), "board", {})
Expand Down
Loading