From 1f3c026b5b22b6ac22b05c2d74affa39d6a20cd4 Mon Sep 17 00:00:00 2001 From: Jev Date: Tue, 17 Feb 2026 21:59:50 +0100 Subject: [PATCH] simplify repo --- test.sh | 6 -- {{cookiecutter.project_slug}}/README.md | 50 +--------- .../examples/config_init.sh | 14 --- {{cookiecutter.project_slug}}/pyproject.toml | 4 +- .../{{cookiecutter.package_name}}/__init__.py | 7 +- .../src/{{cookiecutter.package_name}}/cli.py | 31 ++++++ .../cli/__init__.py | 1 - .../{{cookiecutter.package_name}}/cli/app.py | 33 ------- .../cli/common.py | 24 ----- .../cli/config.py | 48 --------- .../cli/hello.py | 24 ----- .../commands/__init__.py | 5 - .../commands/hello.py | 6 -- .../config/__init__.py | 29 ------ .../config/paths.py | 29 ------ .../config/settings.py | 98 ------------------- .../models/__init__.py | 1 - .../utils/__init__.py | 1 - .../utils/logging.py | 17 ---- .../tests/conftest.py | 22 ----- .../tests/test_cli.py | 73 +++----------- .../tests/test_internals.py | 62 ------------ .../tests/test_public.py | 19 ---- 23 files changed, 46 insertions(+), 558 deletions(-) delete mode 100644 {{cookiecutter.project_slug}}/examples/config_init.sh create mode 100644 {{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/cli.py delete mode 100644 {{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/cli/__init__.py delete mode 100644 {{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/cli/app.py delete mode 100644 {{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/cli/common.py delete mode 100644 {{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/cli/config.py delete mode 100644 {{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/cli/hello.py delete mode 100644 {{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/commands/__init__.py delete mode 100644 {{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/commands/hello.py delete mode 100644 {{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/config/__init__.py delete mode 100644 {{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/config/paths.py delete mode 100644 {{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/config/settings.py delete mode 100644 {{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/models/__init__.py delete mode 100644 {{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/utils/__init__.py delete mode 100644 {{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/utils/logging.py delete mode 100644 {{cookiecutter.project_slug}}/tests/conftest.py delete mode 100644 {{cookiecutter.project_slug}}/tests/test_internals.py delete mode 100644 {{cookiecutter.project_slug}}/tests/test_public.py diff --git a/test.sh b/test.sh index 0c8501b..0475b83 100755 --- a/test.sh +++ b/test.sh @@ -179,12 +179,6 @@ main() { print_status "Testing invoke test task..." uv run invoke test || { print_error "Invoke test task failed"; exit 1; } - # Check if example script runs - if [ -f "examples/config_init.sh" ]; then - print_status "Running example script..." - bash examples/config_init.sh || print_warning "Example script failed (this may be expected if it's a placeholder)" - fi - print_status "✅ All tests passed successfully!" print_status "Template is working correctly." } diff --git a/{{cookiecutter.project_slug}}/README.md b/{{cookiecutter.project_slug}}/README.md index 6cd3a9d..112af73 100644 --- a/{{cookiecutter.project_slug}}/README.md +++ b/{{cookiecutter.project_slug}}/README.md @@ -12,59 +12,13 @@ uv sync --frozen --group dev ```bash {{ cookiecutter.project_slug }} --help -{{ cookiecutter.project_slug }} config show -{{ cookiecutter.project_slug }} config init -``` - -## Configuration - -Default config path (XDG): - -``` -~/.config/{{ cookiecutter.project_slug }}/config.toml -``` - -Override with: - -```bash -export {{ cookiecutter.package_name | upper }}_CONFIG=/path/to/config.toml -``` - -Resolution order: -1. `{{ cookiecutter.package_name | upper }}_CONFIG` explicit path (errors if missing) -2. `~/.config/{{ cookiecutter.project_slug }}/config.toml` when present -3. In-code defaults - -## Reproducible Workflow - -```bash -uv lock -uv sync --frozen --group dev -uv run invoke lint -uv run invoke test +{{ cookiecutter.project_slug }} hello ``` ## Development -### Setup ```bash -uv sync --frozen --group dev -``` - -### Code Quality -```bash -uv run ruff check src tests -uv run ruff format --check src tests -uv run mypy src uv run invoke lint -``` - -### Testing -```bash -uv run pytest uv run invoke test +uv run invoke format ``` - -## License - -MIT License - see LICENSE file for details. diff --git a/{{cookiecutter.project_slug}}/examples/config_init.sh b/{{cookiecutter.project_slug}}/examples/config_init.sh deleted file mode 100644 index 934f455..0000000 --- a/{{cookiecutter.project_slug}}/examples/config_init.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -EXAMPLE_ROOT="${TMPDIR:-/tmp}/{{ cookiecutter.project_slug }}-example" -mkdir -p "$EXAMPLE_ROOT/config" "$EXAMPLE_ROOT/data" - -XDG_CONFIG_HOME="$EXAMPLE_ROOT/config" \ -XDG_DATA_HOME="$EXAMPLE_ROOT/data" \ -uv run {{ cookiecutter.project_slug }} config init --overwrite - -XDG_CONFIG_HOME="$EXAMPLE_ROOT/config" \ -XDG_DATA_HOME="$EXAMPLE_ROOT/data" \ -uv run {{ cookiecutter.project_slug }} config show diff --git a/{{cookiecutter.project_slug}}/pyproject.toml b/{{cookiecutter.project_slug}}/pyproject.toml index 973f709..a1e6454 100644 --- a/{{cookiecutter.project_slug}}/pyproject.toml +++ b/{{cookiecutter.project_slug}}/pyproject.toml @@ -8,13 +8,11 @@ authors = [ ] requires-python = ">=3.12" dependencies = [ - "coloredlogs>=15.0", - "pydantic>=2.0", "typer>=0.12", ] [project.scripts] -{{ cookiecutter.project_slug }} = "{{ cookiecutter.package_name }}.cli.app:app" +{{ cookiecutter.project_slug }} = "{{ cookiecutter.package_name }}.cli:app" [build-system] requires = ["uv_build>=0.8.8,<0.9.0"] diff --git a/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/__init__.py b/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/__init__.py index c6e2ee8..bfec8f4 100644 --- a/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/__init__.py +++ b/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/__init__.py @@ -4,12 +4,7 @@ from __future__ import annotations from importlib.metadata import PackageNotFoundError, version -from {{ cookiecutter.package_name }}.config import get_config -from {{ cookiecutter.package_name }}.config.settings import Settings - try: __version__ = version("{{ cookiecutter.project_slug }}") -except PackageNotFoundError: # pragma: no cover - only in editable/uninstalled mode +except PackageNotFoundError: # pragma: no cover __version__ = "0.0.0" - -__all__ = ["Settings", "__version__", "get_config"] diff --git a/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/cli.py b/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/cli.py new file mode 100644 index 0000000..10079fc --- /dev/null +++ b/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/cli.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from typing import Annotated + +import typer + +app = typer.Typer(no_args_is_help=True) + + +def _version_callback(value: bool) -> None: + if value: + from importlib.metadata import version + + print(version("{{ cookiecutter.project_slug }}")) + raise typer.Exit() + + +@app.callback() +def main( + _version: Annotated[ + bool, + typer.Option("--version", "-v", help="Show version and exit.", callback=_version_callback), + ] = False, +) -> None: + """{{ cookiecutter.description }}""" + + +@app.command() +def hello(name: str = "world") -> None: + """Say hello.""" + print(f"Hello, {name}!") diff --git a/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/cli/__init__.py b/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/cli/__init__.py deleted file mode 100644 index c2a0794..0000000 --- a/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/cli/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""CLI package for {{ cookiecutter.project_name }}.""" diff --git a/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/cli/app.py b/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/cli/app.py deleted file mode 100644 index bbd727d..0000000 --- a/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/cli/app.py +++ /dev/null @@ -1,33 +0,0 @@ -from __future__ import annotations - -from typing import Annotated - -import typer - -from {{ cookiecutter.package_name }}.cli.config import app as config_app -from {{ cookiecutter.package_name }}.cli.hello import app as hello_app -from {{ cookiecutter.package_name }}.utils.logging import setup_logging - -app = typer.Typer(no_args_is_help=True) -app.add_typer(config_app, name="config") -app.add_typer(hello_app, name="hello") - - -@app.callback(invoke_without_command=True) -def main( - version: Annotated[ - bool, - typer.Option("--version", "-v", help="Show version and exit."), - ] = False, -) -> None: - """Run the CLI application.""" - setup_logging() - if version: - from importlib.metadata import version as get_version - - print(get_version("{{ cookiecutter.project_slug }}")) - raise typer.Exit() - - -if __name__ == "__main__": - app() diff --git a/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/cli/common.py b/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/cli/common.py deleted file mode 100644 index 8063503..0000000 --- a/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/cli/common.py +++ /dev/null @@ -1,24 +0,0 @@ -from __future__ import annotations - -import tomllib -from typing import NoReturn - -import typer -from pydantic import ValidationError - -from {{ cookiecutter.package_name }}.config import get_config -from {{ cookiecutter.package_name }}.config.settings import Settings - - -def load_config() -> Settings: - """Load configuration once at the CLI boundary.""" - try: - return get_config() - except (FileNotFoundError, tomllib.TOMLDecodeError, ValidationError) as exc: - exit_with_error(str(exc)) - - -def exit_with_error(message: str, code: int = 1) -> NoReturn: - """Print a concise error and exit with a non-zero status.""" - typer.secho(message, fg=typer.colors.RED, err=True) - raise typer.Exit(code) diff --git a/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/cli/config.py b/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/cli/config.py deleted file mode 100644 index 8c614c7..0000000 --- a/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/cli/config.py +++ /dev/null @@ -1,48 +0,0 @@ -from __future__ import annotations - -import json -from pathlib import Path -from typing import Annotated - -import typer - -from {{ cookiecutter.package_name }}.cli.common import exit_with_error, load_config -from {{ cookiecutter.package_name }}.config import get_config_source -from {{ cookiecutter.package_name }}.config.paths import config_file_path -from {{ cookiecutter.package_name }}.config.settings import write_default_config - -app = typer.Typer(no_args_is_help=True) - - -@app.command() -def show() -> None: - """Print the effective configuration and source path.""" - config = load_config() - source = get_config_source() - payload = { - "source": str(source) if source else "", - "config": config.model_dump(), - } - print(json.dumps(payload, indent=2, sort_keys=True)) - - -@app.command() -def init( - path: Annotated[ - Path | None, - typer.Option( - "--path", - help="Write the default config to a specific path.", - ), - ] = None, - overwrite: Annotated[ - bool, - typer.Option("--overwrite", help="Overwrite existing file."), - ] = False, -) -> None: - """Write a default config to the XDG location.""" - target = path or config_file_path() - if target.exists() and not overwrite: - exit_with_error(f"Config already exists at {target}. Use --overwrite to replace it.") - write_default_config(target) - typer.secho(f"Wrote default config to {target}", fg=typer.colors.GREEN) diff --git a/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/cli/hello.py b/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/cli/hello.py deleted file mode 100644 index 38af0d9..0000000 --- a/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/cli/hello.py +++ /dev/null @@ -1,24 +0,0 @@ -from __future__ import annotations - -from typing import Annotated - -import typer - -from {{ cookiecutter.package_name }}.cli.common import load_config -from {{ cookiecutter.package_name }}.commands import format_greeting - -app = typer.Typer(no_args_is_help=True) - - -@app.command() -def say( - name: Annotated[str, typer.Option("--name", "-n", help="Name to greet.")] = "World", - greeting: Annotated[ - str | None, - typer.Option("--greeting", "-g", help="Greeting prefix; overrides config value."), - ] = None, -) -> None: - """Print a greeting.""" - config = load_config() - message = format_greeting(name=name, greeting=greeting or config.app.greeting) - print(message) diff --git a/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/commands/__init__.py b/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/commands/__init__.py deleted file mode 100644 index ccaee63..0000000 --- a/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/commands/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Command implementations for {{ cookiecutter.project_name }}.""" - -from {{ cookiecutter.package_name }}.commands.hello import format_greeting - -__all__ = ["format_greeting"] diff --git a/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/commands/hello.py b/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/commands/hello.py deleted file mode 100644 index 556c4a6..0000000 --- a/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/commands/hello.py +++ /dev/null @@ -1,6 +0,0 @@ -from __future__ import annotations - - -def format_greeting(name: str, greeting: str = "Hello") -> str: - """Return a greeting message.""" - return f"{greeting}, {name}!" diff --git a/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/config/__init__.py b/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/config/__init__.py deleted file mode 100644 index 1cbc882..0000000 --- a/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/config/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -from __future__ import annotations - -from functools import lru_cache -from pathlib import Path - -from .settings import Settings, load_settings - -CONFIG_SOURCE: Path | None = None - - -@lru_cache -def get_config() -> Settings: - """Return the cached Settings instance.""" - global CONFIG_SOURCE - settings, source = load_settings() - CONFIG_SOURCE = source - return settings - - -def get_config_source() -> Path | None: - """Return the path used to load config, if any.""" - return CONFIG_SOURCE - - -def clear_config_cache() -> None: - """Clear cached settings for tests or reload scenarios.""" - global CONFIG_SOURCE - CONFIG_SOURCE = None - get_config.cache_clear() diff --git a/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/config/paths.py b/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/config/paths.py deleted file mode 100644 index f088bf6..0000000 --- a/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/config/paths.py +++ /dev/null @@ -1,29 +0,0 @@ -from __future__ import annotations - -import os -from pathlib import Path - -APP_NAME = "{{ cookiecutter.project_slug }}" -ENV_CONFIG_VAR = "{{ cookiecutter.package_name | upper }}_CONFIG" - - -def xdg_config_dir() -> Path: - """Return the XDG config directory.""" - root = os.environ.get("XDG_CONFIG_HOME") - return Path(root) if root else Path.home() / ".config" - - -def xdg_data_dir() -> Path: - """Return the XDG data directory.""" - root = os.environ.get("XDG_DATA_HOME") - return Path(root) if root else Path.home() / ".local" / "share" - - -def config_file_path() -> Path: - """Return the default config file path.""" - return xdg_config_dir() / APP_NAME / "config.toml" - - -def data_dir_path() -> Path: - """Return the default data directory path.""" - return xdg_data_dir() / APP_NAME diff --git a/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/config/settings.py b/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/config/settings.py deleted file mode 100644 index 96a5acd..0000000 --- a/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/config/settings.py +++ /dev/null @@ -1,98 +0,0 @@ -from __future__ import annotations - -import os -import tomllib -from pathlib import Path - -from pydantic import BaseModel, Field - -from .paths import ENV_CONFIG_VAR, config_file_path, data_dir_path - - -class AppConfig(BaseModel): - model_config = {"frozen": True, "extra": "forbid"} - - greeting: str = "Hello" - - -class DatabaseConfig(BaseModel): - model_config = {"frozen": True, "extra": "forbid"} - - path: str = Field(default_factory=lambda: str(data_dir_path() / "data.db")) - - -class Settings(BaseModel): - model_config = {"frozen": True, "extra": "forbid"} - - app: AppConfig = AppConfig() - database: DatabaseConfig = DatabaseConfig() - - -def load_settings() -> tuple[Settings, Path | None]: - """Load settings and return the source path, if any.""" - env_path = os.environ.get(ENV_CONFIG_VAR) - if env_path: - path = Path(env_path).expanduser() - if not path.exists(): - raise FileNotFoundError(f"Config file not found: {path}") - data = tomllib.loads(path.read_text()) - return Settings.model_validate(data), path - - default_path = config_file_path() - if default_path.exists(): - data = tomllib.loads(default_path.read_text()) - return Settings.model_validate(data), default_path - - return Settings(), None - - -def write_default_config(path: Path) -> None: - """Write default config to a file.""" - path.parent.mkdir(parents=True, exist_ok=True) - payload = Settings().model_dump() - content = _to_toml(payload) - path.write_text(content) - - -def _to_toml(payload: dict[str, object]) -> str: - lines: list[str] = [] - - for key, value in payload.items(): - if isinstance(value, dict): - _write_table(lines, key, value) - else: - lines.append(f"{key} = {_toml_value(value)}") - - return "\n".join(lines).rstrip() + "\n" - - -def _write_table(lines: list[str], table_name: str, values: dict[str, object]) -> None: - lines.append(f"[{table_name}]") - scalar_items: list[tuple[str, object]] = [] - nested_items: list[tuple[str, dict[str, object]]] = [] - - for key, value in values.items(): - if isinstance(value, dict): - nested_items.append((key, value)) - else: - scalar_items.append((key, value)) - - for key, value in scalar_items: - lines.append(f"{key} = {_toml_value(value)}") - - for key, nested in nested_items: - lines.append("") - _write_table(lines, f"{table_name}.{key}", nested) - - lines.append("") - - -def _toml_value(value: object) -> str: - if isinstance(value, str): - escaped = value.replace("\\", "\\\\").replace('"', '\\"') - return f'"{escaped}"' - if isinstance(value, bool): - return "true" if value else "false" - if isinstance(value, int | float): - return str(value) - raise TypeError(f"Unsupported TOML value: {value!r}") diff --git a/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/models/__init__.py b/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/models/__init__.py deleted file mode 100644 index 4501fca..0000000 --- a/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/models/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Data models for {{ cookiecutter.project_name }}.""" diff --git a/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/utils/__init__.py b/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/utils/__init__.py deleted file mode 100644 index 296f638..0000000 --- a/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/utils/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Utility helpers for {{ cookiecutter.project_name }}.""" diff --git a/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/utils/logging.py b/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/utils/logging.py deleted file mode 100644 index a807748..0000000 --- a/{{cookiecutter.project_slug}}/src/{{cookiecutter.package_name}}/utils/logging.py +++ /dev/null @@ -1,17 +0,0 @@ -from __future__ import annotations - -import logging -import os - -import coloredlogs # type: ignore[import-untyped] - - -def setup_logging(level: str | None = None) -> None: - """Configure application logging.""" - level = level or os.environ.get("LOGLEVEL", "INFO") - coloredlogs.install( - level=level, - fmt="%(asctime)s %(name)s %(levelname)s %(message)s", - datefmt="%H:%M:%S", - ) - logging.getLogger("httpx").setLevel(logging.WARNING) diff --git a/{{cookiecutter.project_slug}}/tests/conftest.py b/{{cookiecutter.project_slug}}/tests/conftest.py deleted file mode 100644 index 67fef55..0000000 --- a/{{cookiecutter.project_slug}}/tests/conftest.py +++ /dev/null @@ -1,22 +0,0 @@ -from __future__ import annotations - -import pytest - -from {{ cookiecutter.package_name }}.config import clear_config_cache -from {{ cookiecutter.package_name }}.config.paths import ENV_CONFIG_VAR - - -@pytest.fixture(autouse=True) -def _isolate_xdg_paths(monkeypatch, tmp_path_factory) -> None: - temp_root = tmp_path_factory.mktemp("xdg") - monkeypatch.setenv("XDG_CONFIG_HOME", str(temp_root / "config")) - monkeypatch.setenv("XDG_DATA_HOME", str(temp_root / "data")) - monkeypatch.delenv(ENV_CONFIG_VAR, raising=False) - yield - - -@pytest.fixture(autouse=True) -def _clear_config_cache() -> None: - clear_config_cache() - yield - clear_config_cache() diff --git a/{{cookiecutter.project_slug}}/tests/test_cli.py b/{{cookiecutter.project_slug}}/tests/test_cli.py index 12bb746..f516882 100644 --- a/{{cookiecutter.project_slug}}/tests/test_cli.py +++ b/{{cookiecutter.project_slug}}/tests/test_cli.py @@ -1,75 +1,24 @@ from __future__ import annotations -import json - from typer.testing import CliRunner -from {{ cookiecutter.package_name }}.cli.app import app -from {{ cookiecutter.package_name }}.config.paths import ENV_CONFIG_VAR +from {{ cookiecutter.package_name }}.cli import app runner = CliRunner() -def test_config_show_success_defaults() -> None: - result = runner.invoke(app, ["config", "show"]) - +def test_help() -> None: + result = runner.invoke(app, ["--help"]) assert result.exit_code == 0 - payload = json.loads(result.output) - assert payload["source"] == "" - assert payload["config"]["app"]["greeting"] == "Hello" -def test_config_init_create_and_overwrite(tmp_path) -> None: - path = tmp_path / "config.toml" - - create_result = runner.invoke(app, ["config", "init", "--path", str(path)]) - assert create_result.exit_code == 0 - assert path.exists() - - conflict_result = runner.invoke(app, ["config", "init", "--path", str(path)]) - assert conflict_result.exit_code == 1 - assert "Config already exists" in conflict_result.output - - overwrite_result = runner.invoke( - app, - ["config", "init", "--path", str(path), "--overwrite"], - ) - assert overwrite_result.exit_code == 0 +def test_version() -> None: + result = runner.invoke(app, ["--version"]) + assert result.exit_code == 0 + assert result.output.strip() -def test_config_show_missing_env_file_exits_with_code_1(tmp_path, monkeypatch) -> None: - missing = tmp_path / "missing.toml" - monkeypatch.setenv(ENV_CONFIG_VAR, str(missing)) - - result = runner.invoke(app, ["config", "show"]) - - assert result.exit_code == 1 - assert "Config file not found" in result.output - - -def test_config_show_invalid_config_exits_with_code_1(tmp_path, monkeypatch) -> None: - config_path = tmp_path / "config.toml" - config_path.write_text('unknown = "value"\n') - monkeypatch.setenv(ENV_CONFIG_VAR, str(config_path)) - - result = runner.invoke(app, ["config", "show"]) - - assert result.exit_code == 1 - assert "Extra inputs are not permitted" in result.output - - -def test_hello_cli_override_precedence(tmp_path, monkeypatch) -> None: - config_path = tmp_path / "config.toml" - config_path.write_text('[app]\ngreeting = "Hi"\n\n[database]\npath = "/tmp/example.db"\n') - monkeypatch.setenv(ENV_CONFIG_VAR, str(config_path)) - - from_config = runner.invoke(app, ["hello", "say", "--name", "Ada"]) - assert from_config.exit_code == 0 - assert "Hi, Ada!" in from_config.output - - from_cli = runner.invoke( - app, - ["hello", "say", "--name", "Ada", "--greeting", "Yo"], - ) - assert from_cli.exit_code == 0 - assert "Yo, Ada!" in from_cli.output +def test_hello() -> None: + result = runner.invoke(app, ["hello", "--name", "Ada"]) + assert result.exit_code == 0 + assert "Hello, Ada!" in result.output diff --git a/{{cookiecutter.project_slug}}/tests/test_internals.py b/{{cookiecutter.project_slug}}/tests/test_internals.py deleted file mode 100644 index b3b7ee3..0000000 --- a/{{cookiecutter.project_slug}}/tests/test_internals.py +++ /dev/null @@ -1,62 +0,0 @@ -from __future__ import annotations - -import pytest -from pydantic import ValidationError - -from {{ cookiecutter.package_name }}.commands import format_greeting -from {{ cookiecutter.package_name }}.config import get_config, get_config_source -from {{ cookiecutter.package_name }}.config.paths import ENV_CONFIG_VAR, config_file_path - - -def test_env_config_override(tmp_path, monkeypatch) -> None: - config_path = tmp_path / "config.toml" - config_path.write_text('[app]\ngreeting = "Hi"\n\n[database]\npath = "/tmp/app.db"\n') - monkeypatch.setenv(ENV_CONFIG_VAR, str(config_path)) - - settings = get_config() - - assert settings.app.greeting == "Hi" - assert settings.database.path == "/tmp/app.db" - assert get_config_source() == config_path - - -def test_missing_env_config_raises(tmp_path, monkeypatch) -> None: - missing = tmp_path / "missing.toml" - monkeypatch.setenv(ENV_CONFIG_VAR, str(missing)) - - with pytest.raises(FileNotFoundError, match="Config file not found"): - get_config() - - -def test_xdg_default_config_is_loaded(monkeypatch) -> None: - path = config_file_path() - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text('[app]\ngreeting = "Hola"\n\n[database]\npath = "/tmp/default.db"\n') - monkeypatch.delenv(ENV_CONFIG_VAR, raising=False) - - settings = get_config() - - assert settings.app.greeting == "Hola" - assert settings.database.path == "/tmp/default.db" - assert get_config_source() == path - - -def test_unknown_key_raises_validation_error(tmp_path, monkeypatch) -> None: - config_path = tmp_path / "config.toml" - config_path.write_text('unknown = "value"\n') - monkeypatch.setenv(ENV_CONFIG_VAR, str(config_path)) - - with pytest.raises(ValidationError): - get_config() - - -def test_settings_are_frozen() -> None: - settings = get_config() - - with pytest.raises(ValidationError, match="frozen"): - settings.app.greeting = "Mutated" - - -def test_format_greeting() -> None: - assert format_greeting("Ada") == "Hello, Ada!" - assert format_greeting("Ada", greeting="Hi") == "Hi, Ada!" diff --git a/{{cookiecutter.project_slug}}/tests/test_public.py b/{{cookiecutter.project_slug}}/tests/test_public.py deleted file mode 100644 index 7039e01..0000000 --- a/{{cookiecutter.project_slug}}/tests/test_public.py +++ /dev/null @@ -1,19 +0,0 @@ -from __future__ import annotations - -from {{ cookiecutter.package_name }} import __all__, __version__, get_config -from {{ cookiecutter.package_name }}.config.settings import Settings - - -def test_public_version_is_string() -> None: - assert isinstance(__version__, str) - - -def test_public_exports_include_stable_symbols() -> None: - assert set(__all__) >= {"__version__", "get_config", "Settings"} - - -def test_get_config_defaults_shape() -> None: - settings = get_config() - assert isinstance(settings, Settings) - assert settings.app.greeting == "Hello" - assert settings.database.path.endswith("/data.db")