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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
/dist/
/docs/_build/
/src/*.egg-info/
*.egg-info/

# Byte-code files
*.pyc
Expand Down
132 changes: 132 additions & 0 deletions .ruff.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@

# Exclude a variety of commonly ignored directories.
# Most importantly, don't lint the inner jinja affected files.
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".ipynb_checkpoints",
".mypy_cache",
".nox",
".pants.d",
".pyenv",
".pytest_cache",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
".vscode",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"site-packages",
"venv",
"[{][{]cookiecutter.project_name[}][}]",
]

# Same as Black.
line-length = 120
indent-width = 4

# Assume Python 3.9
target-version = "py39"

[lint]
# https://docs.astral.sh/ruff/rules
select = [
"A",
"ARG",
"B",
"B9",
"BLE",
"C",
"C4",
"D",
"DTZ",
"E",
"F",
"I",
"N",
"PT",
"PTH",
"Q",
"RET",
"RUF",
"S",
"SIM",
"SLF",
"T10",
"TC",
"W"
]
ignore = ["E203", "E501"]


# Allow fix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
unfixable = []

# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"

[lint.flake8-quotes]
inline-quotes = "double"

[lint.isort]
force-single-line = true
lines-after-imports = 2

[lint.mccabe]
max-complexity = 10

[lint.per-file-ignores]
"tests/*" = [
"S101",
"D100",
"D101",
"D102",
"D103",
"D104",
"ARG001",
"C408",
"SLF001"
]
"exceptions.py" = ["D107"]
"noxfile.py" = ["S101"]

[lint.pydocstyle]
convention = "google"

[format]
# Like Black, use double quotes for strings.
quote-style = "double"

# Like Black, indent with spaces, rather than tabs.
indent-style = "space"

# Like Black, respect magic trailing commas.
skip-magic-trailing-comma = false

# Like Black, automatically detect the appropriate line ending.
line-ending = "auto"

# Enable auto-formatting of code examples in docstrings. Markdown,
# reStructuredText code/literal blocks and doctests are all supported.
#
# This is currently disabled by default, but it is planned for this
# to be opt-out in the future.
docstring-code-format = false

# Set the line length limit used when formatting code snippets in
# docstrings.
#
# This only has an effect when the `docstring-code-format` setting is
# enabled.
docstring-code-line-length = "dynamic"
17 changes: 14 additions & 3 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from datetime import date


# --- Project information -----------------------------------------------------

# General info about the TEMPLATE documentation itself
Expand Down Expand Up @@ -59,8 +60,19 @@

# MyST-Parser settings (allows using Markdown + Sphinx features)
myst_enable_extensions = [
"amsmath", "colon_fence", "deflist", "dollarmath", "html_admonition", "html_image", "replacements",
"smartquotes", "strikethrough", "substitution", "tasklist", "attrs_inline", "attrs_block",
"amsmath",
"colon_fence",
"deflist",
"dollarmath",
"html_admonition",
"html_image",
"replacements",
"smartquotes",
"strikethrough",
"substitution",
"tasklist",
"attrs_inline",
"attrs_block",
]

# Intersphinx mapping: Link to documentation of standard libraries or tools mentioned
Expand All @@ -85,7 +97,6 @@
"sidebar_hide_name": True, # Hide project name next to logo if logo contains it
# "light_logo": "logo-light.png",
# "dark_logo": "logo-dark.png",

# Footer icons
"footer_icons": [
{
Expand Down
31 changes: 13 additions & 18 deletions noxfile.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""Noxfile for the cookiecutter-robust-python template."""
from pathlib import Path

import shutil
import tempfile
from pathlib import Path

import nox
import platformdirs
Expand Down Expand Up @@ -41,11 +42,7 @@
*("--demo-name", DEFAULT_DEMO_NAME),
)

TEMPLATE_PYTHON_LOCATIONS: tuple[Path, ...] = (
Path("noxfile.py"),
Path("scripts/*"),
Path("hooks/*")
)
TEMPLATE_PYTHON_LOCATIONS: tuple[Path, ...] = (Path("noxfile.py"), Path("scripts"), Path("hooks"))

TEMPLATE_CONFIG_AND_DOCS: tuple[Path, ...] = (
Path("pyproject.toml"),
Expand All @@ -59,7 +56,7 @@
Path("LICENSE"),
Path("CODE_OF_CONDUCT.md"),
Path("CHANGELOG.md"),
Path("docs/")
Path("docs/"),
)


Expand All @@ -84,6 +81,7 @@ def sync_uv_with_demo(session: Session) -> None:
external=True,
)


@nox.session(name="uv-in-demo", python=DEFAULT_TEMPLATE_PYTHON_VERSION)
def uv_in_demo(session: Session) -> None:
session.install("cookiecutter", "platformdirs", "loguru", "typer")
Expand Down Expand Up @@ -134,21 +132,20 @@ def clear_cache(session: Session) -> None:
def lint(session: Session):
"""Lint the template's own Python files and configurations."""
session.log("Installing linting dependencies for the template source...")
session.run("uv", "sync", "--locked", "--group", "dev", "--group", "lint", external=True)
session.install("-e", ".", "--group", "dev", "--group", "lint")

locations: list[str] = [str(loc) for loc in TEMPLATE_PYTHON_LOCATIONS + TEMPLATE_CONFIG_AND_DOCS]
session.log(f"Running Ruff formatter check on template files with py{session.python}.")
session.run("uv", "run", "ruff", "format", *locations, "--check", external=True)
session.run("ruff", "format")

session.log(f"Running Ruff check on template files with py{session.python}.")
session.run("uv", "run", "ruff", "check", *locations, "--verbose", external=True)
session.run("ruff", "check", "--verbose", "--fix")


@nox.session(python=DEFAULT_TEMPLATE_PYTHON_VERSION)
def docs(session: Session):
"""Build the template documentation website."""
session.log("Installing documentation dependencies for the template docs...")
session.run("uv", "sync", "--locked", "--group", "dev", "--group", "docs", external=True)
session.install("-e", ".", "--group", "dev", "--group", "docs")

session.log(f"Building template documentation with py{session.python}.")
# Set path to allow Sphinx to import from template root if needed (e.g., __version__.py)
Expand Down Expand Up @@ -178,7 +175,7 @@ def test(session: Session) -> None:
session.log("Running template tests...")
session.log("Installing template testing dependencies...")
# Sync deps from template's own pyproject.toml, e.g., 'dev' group that includes 'pytest', 'cookiecutter'
session.run("uv", "sync", "--locked", "--group", "dev", "--group", "test", external=True)
session.install("-e", ".", "--group", "dev", "--group", "test")

# Create a temporary directory for the generated project
temp_dir: Path = Path(tempfile.mkdtemp())
Expand All @@ -188,19 +185,18 @@ def test(session: Session) -> None:
# Need to find cookiecutter executable - it's in the template dev env installed by uv sync.
cookiecutter_command: list[str] = ["uv", "run", "cookiecutter", "--no-input", "--output-dir", str(temp_dir), "."]


session.run(*cookiecutter_command, external=True)

# Navigate into the generated project directory
generated_project_dir = temp_dir / "test_project" # Use the slug defined in --extra-context
generated_project_dir = temp_dir / "test_project" # Use the slug defined in --extra-context
if not generated_project_dir.exists():
session.error(f"Generated project directory not found: {generated_project_dir}")

session.log(f"Changing to generated project directory: {generated_project_dir}")
session.cd(generated_project_dir)

session.log("Installing generated project dependencies using uv sync...")
session.run("uv", "sync", "--locked", external=True)
session.install("-e", ".", external=True)

session.log("Running generated project's default checks...")
session.run("uv", "run", "nox", external=True)
Expand Down Expand Up @@ -236,12 +232,11 @@ def release_template(session: Session):
cz_bump_args = ["uvx", "cz", "bump", "--changelog"]

if increment:
cz_bump_args.append(f"--increment={increment}")
cz_bump_args.append(f"--increment={increment}")

session.log("Running cz bump with args: %s", cz_bump_args)
# success_codes=[0, 1] -> Allows code 1 which means 'nothing to bump' if no conventional commits since last release
session.run(*cz_bump_args, success_codes=[0, 1], external=True)

session.log("Template version bumped and tag created locally via Commitizen/uvx.")
session.log("IMPORTANT: Push commits and tags to remote (`git push --follow-tags`) to trigger CD for the TEMPLATE.")

10 changes: 3 additions & 7 deletions scripts/generate-demo-project.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,12 @@
from typing import Annotated

import typer

from cookiecutter.main import cookiecutter
from typer.models import OptionInfo


FolderOption: partial[OptionInfo] = partial(
typer.Option,
dir_okay=True,
file_okay=False,
resolve_path=True,
path_type=Path
typer.Option, dir_okay=True, file_okay=False, resolve_path=True, path_type=Path
)


Expand Down Expand Up @@ -47,7 +43,7 @@ def _remove_any_existing_demo(parent_path: Path) -> None:
def main(
repo_folder: Annotated[Path, FolderOption("--repo-folder", "-r")],
demos_cache_folder: Annotated[Path, FolderOption("--demos-cache-folder", "-c")],
demo_name: Annotated[str, typer.Option("--demo-name", "-d")]
demo_name: Annotated[str, typer.Option("--demo-name", "-d")],
) -> None:
"""Updates the poetry.lock file."""
try:
Expand Down
1 change: 0 additions & 1 deletion scripts/prepare-github-release.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,6 @@ def main(
description from the draft release notes. The branch contains a single
commit which updates the version number in the documentation.
"""

if tag is None:
today = datetime.date.today()
tag = f"{today:%Y.%-m.%-d}"
Expand Down
13 changes: 4 additions & 9 deletions scripts/sync-uv-with-demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@
from typing import Annotated

import typer

from loguru import logger
from typer.models import OptionInfo

from loguru import logger

FolderOption: partial[OptionInfo] = partial(
typer.Option, dir_okay=True, file_okay=False, resolve_path=True, path_type=Path
Expand All @@ -22,9 +21,7 @@ def sync_uv_with_demo(template_folder: Path, demos_cache_folder: Path, demo_name
demo_uv_lock_path: Path = _find_uv_lock_path(demo_root)
output_uv_lock_path: Path = _find_uv_lock_path(template_folder)

_copy_uv_lock_from_demo(
demo_uv_lock_path=demo_uv_lock_path, output_uv_lock_path=output_uv_lock_path
)
_copy_uv_lock_from_demo(demo_uv_lock_path=demo_uv_lock_path, output_uv_lock_path=output_uv_lock_path)
logger.info(f"Copied demo from {demo_uv_lock_path=} to {output_uv_lock_path=}.")


Expand All @@ -49,13 +46,11 @@ def _find_uv_lock_path(search_root: Path) -> Path:
def main(
template_folder: Annotated[Path, FolderOption("--template-folder", "-t", exists=True)],
demos_cache_folder: Annotated[Path, FolderOption("--demos-cache-folder", "-c", exists=True)],
demo_name: Annotated[str, typer.Option("--demo-name", "-d")]
demo_name: Annotated[str, typer.Option("--demo-name", "-d")],
) -> None:
"""Updates the uv.lock file."""
try:
sync_uv_with_demo(
template_folder=template_folder, demos_cache_folder=demos_cache_folder, demo_name=demo_name
)
sync_uv_with_demo(template_folder=template_folder, demos_cache_folder=demos_cache_folder, demo_name=demo_name)
except Exception as error:
typer.secho(f"error: {error}", fg="red")
sys.exit(1)
Expand Down
11 changes: 2 additions & 9 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import os
import subprocess

from pathlib import Path
from typing import Generator

Expand All @@ -25,10 +24,7 @@ def robust_python_demo_path(tmp_path_factory: TempPathFactory) -> Path:
no_input=True,
overwrite_if_exists=True,
output_dir=demos_path,
extra_context={
"project_name": "robust-python-demo",
"add_rust_extension": False
}
extra_context={"project_name": "robust-python-demo", "add_rust_extension": False},
)
path: Path = demos_path / "robust-python-demo"
subprocess.run(["uv", "lock"], cwd=path)
Expand All @@ -44,10 +40,7 @@ def robust_maturin_demo_path(tmp_path_factory: TempPathFactory) -> Path:
no_input=True,
overwrite_if_exists=True,
output_dir=demos_path,
extra_context={
"project_name": "robust-maturin-demo",
"add_rust_extension": True
}
extra_context={"project_name": "robust-maturin-demo", "add_rust_extension": True},
)
path: Path = demos_path / "robust-maturin-demo"
subprocess.run(["uv", "sync"], cwd=path)
Expand Down
Loading