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
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ instructions override repository files.
- Keep `src/fgcz_reference_project/__init__.py` empty; import from concrete
modules.
- Pyright strict mode and 100% line/branch coverage are blocking.
- CLIs use `cyclopts`; logging uses `loguru`. Library modules only emit log
records; the CLI entry point configures sinks and levels.
- Add domain-specific checks as hooks, then invoke the same hooks from CI.
- Record user-visible changes under `Unreleased` in `CHANGELOG.md`.
- `.ci/check-tests-structure/` is vendored, MIT-licensed, and keeps its own
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Changed

- The example CLI uses `cyclopts` and logs with `loguru` (configured in the CLI,
emitted by the library), replacing `argparse`; `--verbose` shows debug logs.
- Documented the reference project's intent and complete quality-gate stack.
- Enabled blocking Ruff complexity and function-design checks.
- Switched the build backend from hatchling to `uv_build`.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ structure and quality gates.
| Area | Included |
|---|---|
| Packaging | `src/` layout, `pyproject.toml`, uv build backend, uv lockfile, typed-package marker, console entry point |
| Example | One typed library function and a small `argparse` CLI |
| Example | One typed library function and a small `cyclopts` CLI with `loguru` logging |
| Tests | Unit and CLI error-path tests with 100% line and branch coverage |
| Documentation | MkDocs Material site built with `--strict` and deployable to GitHub Pages |
| Agent guidance | Verified root and scoped `AGENTS.md` files with Claude/Gemini symlinks |
Expand Down
7 changes: 5 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ license = "Apache-2.0"
authors = [
{ name = "Functional Genomics Center Zurich" },
]
dependencies = []
dependencies = [
"cyclopts>=5.2,<6",
"loguru>=0.7.3,<0.8",
]

[project.scripts]
fgcz-reference = "fgcz_reference_project.cli:main"
Expand Down Expand Up @@ -107,7 +110,7 @@ show_missing = true
[tool.deptry]
known_first_party = ["fgcz_reference_project"]
# The vendored hook in .ci/ is a separate distribution with its own dependencies;
# deptry would otherwise report them against this zero-dependency project.
# deptry would otherwise report them against this project.
extend_exclude = ["\\.ci"]

[tool.check-tests-structure]
Expand Down
49 changes: 34 additions & 15 deletions src/fgcz_reference_project/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,48 @@

from __future__ import annotations

import argparse
import sys
from typing import TYPE_CHECKING

import cyclopts
from loguru import logger

from fgcz_reference_project import naming

if TYPE_CHECKING:
from collections.abc import Sequence

app = cyclopts.App(
name="fgcz-reference",
help="Normalize a project name to a stable slug.",
)


def _parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="fgcz-reference",
description="Normalize a project name to a stable slug.",
)
parser.add_argument("name", help="Human-readable project name.")
return parser
@app.default
def slug(name: str, *, verbose: bool = False) -> int:
"""Print the stable slug for a project name.
Args:
name: Human-readable project name.
verbose: Also log debug messages to stderr.
def main() -> int:
"""Run the example command-line interface."""
parser = _parser()
arguments = parser.parse_args()
Returns:
Exit code: 0 on success, 1 when the name has no letters or digits.
"""
# The CLI owns logging configuration; library modules only emit records.
logger.remove()
logger.add(sys.stderr, level="DEBUG" if verbose else "INFO")

try:
slug = naming.project_slug(arguments.name)
result = naming.project_slug(name)
except ValueError as error:
parser.error(str(error))
logger.error(str(error))
return 1

print(slug)
print(result)
return 0


def main(argv: Sequence[str] | None = None) -> int:
"""Run the command-line interface and return its exit code."""
return app(argv, result_action="return_value")
3 changes: 3 additions & 0 deletions src/fgcz_reference_project/naming.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import re

from loguru import logger

_NON_ALPHANUMERIC = re.compile(r"[^a-z0-9]+")


Expand All @@ -23,4 +25,5 @@ def project_slug(name: str) -> str:
if not slug:
message = "Project name must contain at least one letter or digit."
raise ValueError(message)
logger.debug("Slug for {!r}: {!r}", name, slug)
return slug
42 changes: 25 additions & 17 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,38 @@
"""Tests for the example command-line interface."""

import sys
from collections.abc import Iterator

import pytest
from loguru import logger

from fgcz_reference_project import cli


def test_main_prints_slug(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
monkeypatch.setattr(sys, "argv", ["fgcz-reference", "Example Project"])
@pytest.fixture(autouse=True)
def _reset_logging() -> Iterator[None]:
yield
logger.remove()

assert cli.main() == 0
assert capsys.readouterr().out == "example-project\n"

def test_main_prints_slug(capsys: pytest.CaptureFixture[str]) -> None:
assert cli.main(["Example Project"]) == 0

def test_main_reports_invalid_name(
monkeypatch: pytest.MonkeyPatch,
capsys: pytest.CaptureFixture[str],
) -> None:
monkeypatch.setattr(sys, "argv", ["fgcz-reference", "///"])
captured = capsys.readouterr()
assert captured.out == "example-project\n"
assert "Slug for" not in captured.err

with pytest.raises(SystemExit) as error:
cli.main()

assert error.value.code == 2
assert "at least one letter or digit" in capsys.readouterr().err
def test_main_verbose_logs_debug(capsys: pytest.CaptureFixture[str]) -> None:
assert cli.main(["--verbose", "Example Project"]) == 0

captured = capsys.readouterr()
assert captured.out == "example-project\n"
assert "Slug for 'Example Project': 'example-project'" in captured.err


def test_main_reports_invalid_name(capsys: pytest.CaptureFixture[str]) -> None:
assert cli.main(["///"]) == 1

captured = capsys.readouterr()
assert captured.out == ""
assert "at least one letter or digit" in captured.err
76 changes: 76 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading