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
56 changes: 29 additions & 27 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,41 +8,45 @@ on:
release:
types: [ published ]

# Every uv invocation asserts uv.lock is already up to date. Run `uv lock` and
# commit the result when changing dependencies.
env:
UV_LOCKED: "1"

jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
fetch-depth: 0 # Fetch all history and tags for version detection

- name: Install uv
uses: astral-sh/setup-uv@v5

# setup-uv no longer publishes a floating major tag, so pin the commit.
- name: Install uv and Python
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
enable-cache: true

- name: Set up Python
run: uv python install 3.13

python-version: "3.14"

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

- name: Lint with ruff
run: uv run ruff check
run: uv run ruff check --output-format github

- name: Check formatting
run: uv run ruff format --check
run: uv run ruff format --check --output-format github

- name: Type check
run: uv run ty check

- name: Run tests
run: uv run pytest

- name: Security audit
run: |
# Just warnings for now...
uv audit --preview-features audit || true
uv audit --preview-features audit || true

build-and-publish:
needs: lint-and-test
Expand All @@ -51,26 +55,24 @@ jobs:
permissions:
contents: write # Required for uploading release assets
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
with:
fetch-depth: 0 # Fetch all history and tags for version detection
- name: Install uv
uses: astral-sh/setup-uv@v5

- name: Install uv and Python
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
with:
enable-cache: true

- name: Set up Python
run: uv python install 3.13

python-version: "3.14"

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

- name: Build package
run: uv run python -m build

- name: Upload artifacts to GitHub Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v3
with:
files: dist/*
fail_on_unmatched_files: true
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Cassidy's opinionated Python template. Enter at your own peril.

Requires [uv](https://docs.astral.sh/uv/)

Setup: `uv sync --dev`
Setup: `uv sync --dev --locked`

Lint: `uv run ruff check`

Expand All @@ -18,6 +18,10 @@ Test: `uv run pytest`

Security audit: `uv audit --preview-features audit`

`--locked` fails if `uv.lock` is stale rather than silently re-resolving. CI
sets `UV_LOCKED=1` so this applies to every uv command. When you change
dependencies, run `uv lock` and commit the result.

## Template Setup

1. **Create a new repository** from this template on GitHub (click "Use this template")
Expand All @@ -44,8 +48,10 @@ Security audit: `uv audit --preview-features audit`
- Replace the title and description
- Remove or customize this Template Setup section

7. **Initialize your environment**:
7. **Initialize your environment** (you just edited `pyproject.toml`, so
refresh the lockfile and commit it):
```bash
uv lock
uv sync --dev
```

Expand Down
42 changes: 21 additions & 21 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,26 @@ readme = "README.md"
authors = [
{ name = "Cassidy Prather", email = "pratherea@gmail.com" }
]
requires-python = ">=3.13, < 3.14"
requires-python = ">=3.14, < 3.15"
dependencies = [
"defusedxml >= 0.7.1, < 1",
"trio >= 0.33.0, < 1",
"trio >= 0.34.0, < 1",
]

[project.scripts]
python-template = "python_template.cli:main"

[dependency-groups]
dev = [
"ruff >= 0.14.3, < 1",
"ty >= 0.0.25, < 1",
"pytest >= 9.0.3, < 10",
"pytest-cov >= 7.0.0, < 8",
"gcovr >= 8.4, < 9",
"build >= 1.2.1, < 2",
"twine >= 6.2.0, < 7",
"ruff >= 0.16.5, < 1",
"ty >= 0.0.75, < 1",
"pytest >= 9.1.1, < 10",
"pytest-cov >= 7.1.0, < 8",
"gcovr >= 8.6, < 9",
"build >= 1.5.0, < 2",
"twine >= 7.0.0, < 8",
"pytest-trio >= 0.8.0, < 1",
"requests >= 2.33.0, < 3",
"requests >= 2.34.2, < 3",
]

[build-system]
Expand Down Expand Up @@ -117,22 +117,22 @@ select = [
]
ignore = [
"D10", # Undocumented stuff (not everything needs a docblock you dumb little [___])
"D203", # Ruff throws a warning if you include both this and D211
"D213", # Ruff throws a warning if you include both this and D212
"DOC201", # Rule is broken and doesn't detect :return: clauses
"DOC501", # Doesn't detect :raises: clauses and fails to ignore abstract
"EM101", # Doesn't account for custom constructors
"incorrect-blank-line-before-class", # Conflicts with `blank-line-before-class`
"multi-line-summary-second-line", # Conflicts with `multi-line-summary-first-line`
"docstring-missing-returns", # Rule is broken and doesn't detect :return: clauses
"docstring-missing-exception", # Doesn't detect :raises: clauses and fails to ignore abstract
"raw-string-in-exception", # Doesn't account for custom constructors
"PLR09", # Complexity begets defects, and these rules promote Complexity
"PLR1702", # Redundant with max line width
"PLR6301", # Rule is broken (doesn't work for implementations of abstract)
"PLR2004", # Magic values aren't always bad. Sometimes, fewer lines are nice
"COM812", # Broken, see: https://github.com/astral-sh/ruff/issues/9216
"TRY400", # ??? https://docs.python.org/3/library/logging.html#logging-levels
"too-many-nested-blocks", # Redundant with max line width
"no-self-use", # Rule is broken (doesn't work for implementations of abstract)
"magic-value-comparison", # Magic values aren't always bad. Sometimes, fewer lines are nice
"missing-trailing-comma", # Broken, see: https://github.com/astral-sh/ruff/issues/9216
"error-instead-of-exception", # ??? https://docs.python.org/3/library/logging.html#logging-levels
]

[tool.ruff.lint.per-file-ignores]
"tests/**/*.py" = [
"S101", # Allow assert in tests
"assert", # Allow `assert` in tests
]

[tool.ruff.format]
Expand Down
2 changes: 1 addition & 1 deletion src/python_template/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
try:
_PACKAGE_METADATA = importlib.metadata.metadata(__package__ or "python_template")
_CLI_NAME = _PACKAGE_METADATA["Name"]
except (importlib.metadata.PackageNotFoundError, KeyError):
except importlib.metadata.PackageNotFoundError, KeyError:
# Fallback if metadata not available (e.g., editable install issues)
_CLI_NAME = "python-template"

Expand Down
7 changes: 5 additions & 2 deletions src/python_template/commands/example.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@
example.add_parser(subparsers, common_parser)
"""

import argparse
import logging
from typing import TYPE_CHECKING

if TYPE_CHECKING:
import argparse

_logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -61,6 +64,6 @@ def run(args: argparse.Namespace) -> int:
_logger.debug("Greeting: %s", args.greeting)

# Your command logic here
print(f"{args.greeting}, {args.name}!") # noqa: T201
print(f"{args.greeting}, {args.name}!") # ruff: ignore[print]

return 0
7 changes: 5 additions & 2 deletions src/python_template/commands/version.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
"""Version subcommand - display package version."""

import argparse
import logging
from typing import TYPE_CHECKING

from python_template import __version__

if TYPE_CHECKING:
import argparse

_logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -35,5 +38,5 @@ def run(_args: argparse.Namespace) -> int:
:return: Exit code (0 for success)
"""
_logger.info("Displaying version information")
print(f"python-template {__version__}") # noqa: T201
print(f"python-template {__version__}") # ruff: ignore[print]
return 0
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ def cli_name() -> str:
# Dynamically get from package metadata if possible
try:
return importlib.metadata.metadata("python-template")["Name"]
except (importlib.metadata.PackageNotFoundError, KeyError):
except importlib.metadata.PackageNotFoundError, KeyError:
return "python-template"
2 changes: 1 addition & 1 deletion tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Tests for CLI parser and entry point."""

import argparse
import subprocess # noqa: S404
import subprocess # ruff: ignore[suspicious-subprocess-import]
import sys
from io import StringIO
from unittest.mock import patch
Expand Down
24 changes: 12 additions & 12 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Integration tests for CLI functionality."""

import re
import subprocess # noqa: S404
import subprocess # ruff: ignore[suspicious-subprocess-import]
import sys

import pytest
Expand All @@ -14,7 +14,7 @@

def test_cli_help() -> None:
"""Test that CLI help works."""
result = subprocess.run( # noqa: S603
result = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true]
[sys.executable, "-m", _PACKAGE_NAME, "--help"],
capture_output=True,
text=True,
Expand All @@ -27,7 +27,7 @@ def test_cli_help() -> None:

def test_cli_version_flag() -> None:
"""Test --version flag."""
result = subprocess.run( # noqa: S603
result = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true]
[sys.executable, "-m", _PACKAGE_NAME, "--version"],
capture_output=True,
text=True,
Expand All @@ -41,7 +41,7 @@ def test_cli_version_flag() -> None:

def test_cli_version_subcommand() -> None:
"""Test version subcommand."""
result = subprocess.run( # noqa: S603
result = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true]
[sys.executable, "-m", _PACKAGE_NAME, "version"],
capture_output=True,
text=True,
Expand All @@ -55,7 +55,7 @@ def test_cli_version_subcommand() -> None:

def test_cli_verbose_flag() -> None:
"""Test that -v flag enables INFO logging."""
result = subprocess.run( # noqa: S603
result = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true]
[sys.executable, "-m", _PACKAGE_NAME, "-v", "version"],
capture_output=True,
text=True,
Expand All @@ -67,7 +67,7 @@ def test_cli_verbose_flag() -> None:

def test_cli_very_verbose_flag() -> None:
"""Test that -vv flag enables DEBUG logging."""
result = subprocess.run( # noqa: S603
result = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true]
[sys.executable, "-m", _PACKAGE_NAME, "-vv", "version"],
capture_output=True,
text=True,
Expand All @@ -80,7 +80,7 @@ def test_cli_very_verbose_flag() -> None:

def test_cli_no_subcommand() -> None:
"""Test that running without subcommand shows help."""
result = subprocess.run( # noqa: S603
result = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true]
[sys.executable, "-m", _PACKAGE_NAME],
capture_output=True,
text=True,
Expand All @@ -92,7 +92,7 @@ def test_cli_no_subcommand() -> None:

def test_cli_invalid_subcommand() -> None:
"""Test that invalid subcommand shows error."""
result = subprocess.run( # noqa: S603
result = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true]
[sys.executable, "-m", _PACKAGE_NAME, "invalid"],
capture_output=True,
text=True,
Expand All @@ -104,7 +104,7 @@ def test_cli_invalid_subcommand() -> None:

def test_version_subcommand_help() -> None:
"""Test that version subcommand has help."""
result = subprocess.run( # noqa: S603
result = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true]
[sys.executable, "-m", _PACKAGE_NAME, "version", "--help"],
capture_output=True,
text=True,
Expand Down Expand Up @@ -146,7 +146,7 @@ def test_library_import_no_cli_execution() -> None:
print("SUCCESS")
"""

result = subprocess.run( # noqa: S603
result = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true]
[sys.executable, "-c", code],
capture_output=True,
text=True,
Expand All @@ -159,7 +159,7 @@ def test_library_import_no_cli_execution() -> None:
@pytest.mark.skip(reason="Example command not registered - template only")
def test_example_subcommand() -> None:
"""Test example subcommand (when registered)."""
result = subprocess.run( # noqa: S603
result = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true]
[sys.executable, "-m", _PACKAGE_NAME, "example", "World"],
capture_output=True,
text=True,
Expand All @@ -172,7 +172,7 @@ def test_example_subcommand() -> None:
@pytest.mark.skip(reason="Example command not registered - template only")
def test_example_subcommand_with_greeting() -> None:
"""Test example subcommand with custom greeting."""
result = subprocess.run( # noqa: S603
result = subprocess.run( # ruff: ignore[subprocess-without-shell-equals-true]
[
sys.executable,
"-m",
Expand Down
Loading