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
35 changes: 35 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: CI

on:
push:
branches: [main, master]
pull_request:
branches: [main, master]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install dependencies
run: uv sync --dev

- name: Run tests
run: uv run pytest

- name: Lint with ruff
run: uv run ruff check .

- name: Type check with mypy
run: uv run mypy .
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ pyproject.toml (Dependencies & tool config)
### Operational Constraints
- **No Interactive Prompts:** Mock or bypass any interactive commands.
- **No Git Operations:** Don't stage/commit unless explicitly requested.
- **Code Review Mode:** Analyze only; record findings in `./REVIEW.md` without making modifications.
- **Code Review Mode:** Analyze only; record findings in `./REVIEW.md` without making modifications. At the top of the review, identify the reviewer including the name of the IDE/CLI used and the primary model that performed the review.

### File Maintenance
- **Keep Instructions Current:** Update "Tech Stack," "Project Structure," and "Workflow Commands" if `pyproject.toml`, structure, or core logic changes.
Expand All @@ -57,5 +57,5 @@ uv run pytest # Run tests
uv run ruff check . # Lint
uv run ruff format . # Auto-format
uv run mypy . # Type check
uv run python -m python_package_template.cli hello # Test CLI
uv run hello-world hello # Test CLI
```
4 changes: 2 additions & 2 deletions AGENTS_MANUAL_CHECKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ pyproject.toml (Dependencies & tool config)
### Operational Constraints
- **No Interactive Prompts:** Mock or bypass any interactive commands.
- **No Git Operations:** Don't stage/commit unless explicitly requested.
- **Code Review Mode:** Analyze only; record findings in `./REVIEW.md` without making modifications.
- **Code Review Mode:** Analyze only; record findings in `./REVIEW.md` without making modifications. At the top of the review, identify the reviewer including the name of the IDE/CLI used and the primary model that performed the review.

### File Maintenance
- **Keep Instructions Current:** Update "Tech Stack," "Project Structure," and "Workflow Commands" if `pyproject.toml`, structure, or core logic changes.
Expand All @@ -57,5 +57,5 @@ uv run pytest # Run tests (USER RUNS)
uv run ruff check . # Lint (USER RUNS)
uv run ruff format . # Auto-format (USER RUNS)
uv run mypy . # Type check (USER RUNS)
uv run python -m python_package_template.cli hello # Test CLI
uv run hello-world hello # Test CLI
```
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,30 @@ This package is intentionally simple to provide a clean starting point for your

### Setup

Clone the repository and install dependencies:
**Option 1: Use this template (recommended)**

Visit https://github.com/AlexAndrewsAI/python-package-template and click the green "Use this template" button to create your own repository. Then clone your new repository:

```bash
cd your-repo-name
uv sync
```

**Option 2: Clone directly**

```bash
git clone https://github.com/AlexAndrewsAI/python-package-template.git
cd python-package-template
uv sync
```

To install the package in editable mode (recommended for development) and test the CLI:

```bash
uv pip install -e .
hello-world --version
```

## Usage

### Basic Example
Expand Down
16 changes: 15 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ dynamic = ["version"] # Version is read from __init__.py by hatch
description = "A simple package using pydantic"
readme = "README.md"
requires-python = ">=3.10"
license = {text = "MIT"}
license = "MIT"
authors = [
{name = "AlexAndrewsAI", email = "alex.andrews.ai@protonmail.com"}
]
Expand All @@ -30,6 +30,7 @@ dev = [
"pytest>=7.0",
"ruff",
"mypy",
"pytest-cov>=7.1.0",
]

[project.scripts]
Expand Down Expand Up @@ -83,3 +84,16 @@ disallow_untyped_defs = true # Require type hints on all function definitions

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "--cov=python_package_template --cov-report=term-missing --cov-fail-under=80"

[tool.coverage.run]
source = ["python_package_template"]
omit = []
parallel = true

[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
]
6 changes: 6 additions & 0 deletions python_package_template/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Main entry point for python -m python_package_template."""

from python_package_template.cli import app

if __name__ == "__main__":
app()
13 changes: 1 addition & 12 deletions python_package_template/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,32 +31,21 @@ def main(
),
) -> None:
"""Python package template CLI."""
pass
...


@app.command()
def hello(
name: str = typer.Option(
"World", "--name", "-n", help="Name to greet (default: World)"
),
version: bool = typer.Option(
False,
"--version",
"-V",
help="Show the version and exit.",
is_eager=True,
),
) -> None:
"""Greet the specified name.

Args:
name: The name to greet.
version: Show version and exit.

"""
if version:
typer.echo(f"python-package-template version: {__version__}")
raise typer.Exit()
config = Config(name=name)
hello_world = HelloWorld(config)
greeting = hello_world.greet()
Expand Down
2 changes: 1 addition & 1 deletion python_package_template/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ class Config(BaseModel):

"""

name: str = Field(default="World", description="The name to greet")
name: str = Field(default="World", min_length=1, description="The name to greet")

model_config = {"title": "Hello World Config", "frozen": True}
39 changes: 29 additions & 10 deletions tests/test_hello.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import logging

import pytest
from pydantic import ValidationError
from typer.testing import CliRunner

from python_package_template import Config, HelloWorld
Expand Down Expand Up @@ -41,6 +42,25 @@ def test_custom_name(caplog: pytest.LogCaptureFixture) -> None:
assert caplog.records[0].levelname == "INFO"


def test_empty_name_validation() -> None:
"""Test that Config validates against empty names."""
with pytest.raises(ValidationError, match="at least 1 character"):
Config(name="")


def test_config_frozen_immutability() -> None:
"""Test that Config is frozen and cannot be modified after creation."""
config = Config(name="Alice")
with pytest.raises(ValidationError, match="Instance is frozen"):
config.name = "Bob"


def test_config_invalid_type() -> None:
"""Test that Config validates against invalid types."""
with pytest.raises(ValidationError, match="Input should be a valid string"):
Config(name=123) # type: ignore[arg-type]


# CLI Tests
runner = CliRunner()

Expand Down Expand Up @@ -80,15 +100,14 @@ def test_cli_version_short() -> None:
assert "python-package-template version:" in result.output


def test_cli_hello_version() -> None:
"""Test CLI hello subcommand --version flag."""
result = runner.invoke(app, ["hello", "--version"])
assert result.exit_code == 0
assert "python-package-template version:" in result.output


def test_cli_hello_empty_name() -> None:
"""Test CLI hello command with empty string name."""
"""Test CLI hello command with empty string name raises validation error."""
result = runner.invoke(app, ["hello", "--name", ""])
assert result.exit_code == 0
assert "Hello, !" in result.output
assert result.exit_code != 0


def test_main_entry_point() -> None:
"""Test that __main__.py can be imported and provides the app."""
from python_package_template import __main__

assert hasattr(__main__, "app")
Loading
Loading