Skip to content

API Reference

This page is auto-generated from Python docstrings.

pup_core

Inspect package.

base

Base package.

errors

Shared exception types.

GitInspectionError

Bases: PupCoreError

Raised when required Git repository information cannot be inspected.

Source code in src/pup_core/base/errors.py
class GitInspectionError(PupCoreError):
    """Raised when required Git repository information cannot be inspected."""
PupCoreError

Bases: Exception

Base exception for pup-core.

Source code in src/pup_core/base/errors.py
class PupCoreError(Exception):
    """Base exception for pup-core."""
PyprojectError

Bases: PupCoreError

Raised when pyproject.toml cannot be read or interpreted.

Source code in src/pup_core/base/errors.py
class PyprojectError(PupCoreError):
    """Raised when pyproject.toml cannot be read or interpreted."""
RepositoryDetectionError

Bases: PupCoreError

Raised when the target repository cannot be determined.

Source code in src/pup_core/base/errors.py
class RepositoryDetectionError(PupCoreError):
    """Raised when the target repository cannot be determined."""
SectionError

Bases: PupCoreError

Raised when a managed file section is malformed or ambiguous.

Source code in src/pup_core/base/errors.py
class SectionError(PupCoreError):
    """Raised when a managed file section is malformed or ambiguous."""
UnsafePathError

Bases: PupCoreError

Raised when a path would escape the target repository.

Source code in src/pup_core/base/errors.py
class UnsafePathError(PupCoreError):
    """Raised when a path would escape the target repository."""

    def __init__(self, path: Path) -> None:
        """Initialize the error."""
        super().__init__(f"Unsafe path escapes repository root: {path}")
__init__
__init__(path: Path) -> None

Initialize the error.

Source code in src/pup_core/base/errors.py
def __init__(self, path: Path) -> None:
    """Initialize the error."""
    super().__init__(f"Unsafe path escapes repository root: {path}")

types

Shared typed records.

GitInfo dataclass

Detected Git repository information.

Source code in src/pup_core/base/types.py
@dataclass(frozen=True)
class GitInfo:
    """Detected Git repository information."""

    origin_url: str
    github_owner: str
    github_repo: str
    tracked_files: frozenset[str]
    untracked_files: frozenset[str]
    ignored_files: frozenset[str]
    dirty: bool
ManagedSection dataclass

A uniquely identified managed section in file text.

Source code in src/pup_core/base/types.py
@dataclass(frozen=True)
class ManagedSection:
    """A uniquely identified managed section in file text."""

    name: str
    start_marker: str
    end_marker: str
    start_index: int
    end_index: int
    content_start: int
    content_end: int
    content: str
PackageInfo dataclass

Detected Python package information.

Source code in src/pup_core/base/types.py
@dataclass(frozen=True)
class PackageInfo:
    """Detected Python package information."""

    src_layout: bool
    packages: tuple[str, ...]
    primary_package: str
PyprojectInfo dataclass

Selected facts read from pyproject.toml.

Source code in src/pup_core/base/types.py
@dataclass(frozen=True)
class PyprojectInfo:
    """Selected facts read from pyproject.toml."""

    path: Path
    project_name: str
    requires_python: str
    dependencies: tuple[str, ...]
    dependency_groups: dict[str, tuple[str, ...]]
    optional_dependencies: dict[str, tuple[str, ...]]
    scripts: dict[str, str]
    build_backend: str
    uv_default_groups: tuple[str, ...] | Literal["all"] | None
    ruff_target_version: str
    pytest_testpaths: tuple[str, ...]
    pytest_addopts: str
    hatch_version_file: str
    hatch_wheel_packages: tuple[str, ...]
RepositoryContext dataclass

Detected information about a target repository.

Source code in src/pup_core/base/types.py
@dataclass(frozen=True)
class RepositoryContext:
    """Detected information about a target repository."""

    root: Path
    github_handle: str
    repo_name: str
    repo_url: str
    site_url: str
    src_package: str
    files: frozenset[str]
SectionPlan dataclass

Planned operation for one explicitly managed section.

Source code in src/pup_core/base/types.py
@dataclass(frozen=True)
class SectionPlan:
    """Planned operation for one explicitly managed section."""

    name: str
    action: SectionAction
    start_marker: str
    end_marker: str
    current: ManagedSection | None
    desired_content: str | None
    insert_before_marker: str | None = None

compare

Shared read-only comparison primitives.

files

Read-only file and history comparison primitives.

FileComparison dataclass

Facts about two corresponding files.

Source code in src/pup_core/compare/files.py
@dataclass(frozen=True)
class FileComparison:
    """Facts about two corresponding files."""

    left_path: Path
    right_path: Path
    relationship: FileRelationship
    left_text: str | None
    right_text: str | None
    left_changed: datetime | None = None
    right_changed: datetime | None = None
classify_file_relationship
classify_file_relationship(
    *,
    left_exists: bool,
    right_exists: bool,
    left_text: str | None,
    right_text: str | None,
    left_changed: datetime | None = None,
    right_changed: datetime | None = None,
) -> FileRelationship

Classify corresponding files using presence, content, and history.

Source code in src/pup_core/compare/files.py
def classify_file_relationship(
    *,
    left_exists: bool,
    right_exists: bool,
    left_text: str | None,
    right_text: str | None,
    left_changed: datetime | None = None,
    right_changed: datetime | None = None,
) -> FileRelationship:
    """Classify corresponding files using presence, content, and history."""
    if left_exists and not right_exists:
        return "left-only"

    if right_exists and not left_exists:
        return "right-only"

    if left_text == right_text:
        return "same"

    if left_changed is not None and right_changed is not None:
        if left_changed > right_changed:
            return "left-newer"

        if right_changed > left_changed:
            return "right-newer"

    return "different"
compare_text_files
compare_text_files(
    *,
    left_path: Path,
    right_path: Path,
    left_changed: datetime | None = None,
    right_changed: datetime | None = None,
) -> FileComparison

Compare two UTF-8 text files without modifying either file.

Source code in src/pup_core/compare/files.py
def compare_text_files(
    *,
    left_path: Path,
    right_path: Path,
    left_changed: datetime | None = None,
    right_changed: datetime | None = None,
) -> FileComparison:
    """Compare two UTF-8 text files without modifying either file."""
    left_text = _read_text(left_path)
    right_text = _read_text(right_path)

    relationship = classify_file_relationship(
        left_exists=left_path.is_file(),
        right_exists=right_path.is_file(),
        left_text=left_text,
        right_text=right_text,
        left_changed=left_changed,
        right_changed=right_changed,
    )

    return FileComparison(
        left_path=left_path,
        right_path=right_path,
        relationship=relationship,
        left_text=left_text,
        right_text=right_text,
        left_changed=left_changed,
        right_changed=right_changed,
    )

data

Packaged pup-core data.

inspect

Inspect package.

actions

GitHub Actions workflow inspection.

ActionReference dataclass

One external action reference in a workflow.

Source code in src/pup_core/inspect/actions.py
@dataclass(frozen=True)
class ActionReference:
    """One external action reference in a workflow."""

    action: str
    ref: str
    version_style: ActionVersionStyle
WorkflowInfo dataclass

Selected facts about one GitHub Actions workflow.

Source code in src/pup_core/inspect/actions.py
@dataclass(frozen=True)
class WorkflowInfo:
    """Selected facts about one GitHub Actions workflow."""

    path: Path
    name: str
    actions: tuple[ActionReference, ...]
inspect_workflow
inspect_workflow(path: Path) -> WorkflowInfo

Inspect one GitHub Actions workflow without modifying it.

Source code in src/pup_core/inspect/actions.py
def inspect_workflow(path: Path) -> WorkflowInfo:
    """Inspect one GitHub Actions workflow without modifying it."""
    text = path.read_text(encoding="utf-8")

    name_match = _NAME_RE.search(text)
    name = name_match.group("name").strip() if name_match else path.stem

    actions = tuple(
        ActionReference(
            action=match.group("action"),
            ref=match.group("ref"),
            version_style=_classify_ref(match.group("ref")),
        )
        for match in _USES_RE.finditer(text)
    )

    return WorkflowInfo(
        path=path,
        name=name,
        actions=actions,
    )
list_workflow_files
list_workflow_files(root: Path) -> tuple[Path, ...]

Return GitHub Actions workflow files in a repository.

Source code in src/pup_core/inspect/actions.py
def list_workflow_files(root: Path) -> tuple[Path, ...]:
    """Return GitHub Actions workflow files in a repository."""
    workflow_root = root / ".github" / "workflows"

    if not workflow_root.is_dir():
        return ()

    return tuple(
        sorted(
            path
            for path in workflow_root.iterdir()
            if path.is_file() and path.suffix.lower() in {".yml", ".yaml"}
        )
    )

catalog

Load the human-readable repository-inspection capability catalog.

load_repository_feature_catalog
load_repository_feature_catalog() -> dict[str, Any]

Return the packaged repository-feature catalog.

Source code in src/pup_core/inspect/catalog.py
def load_repository_feature_catalog() -> dict[str, Any]:
    """Return the packaged repository-feature catalog."""
    resource = files("pup_core.data").joinpath("repository_features.toml")

    with resource.open("rb") as stream:
        return tomllib.load(stream)

detect

Repository detection.

detect_repository
detect_repository(
    root: Path | None = None,
) -> RepositoryContext

Detect objective facts about a repository.

Source code in src/pup_core/inspect/detect.py
def detect_repository(root: Path | None = None) -> RepositoryContext:
    """Detect objective facts about a repository."""
    repo_root = resolve_repository_root(root)
    files = snapshot_repository_files(repo_root)
    git_info = inspect_git(repo_root)

    github_handle = git_info.github_owner
    repo_name = git_info.github_repo or repo_root.name

    if github_handle:
        repo_url = f"https://github.com/{github_handle}/{repo_name}"
        site_url = f"https://{github_handle}.github.io/{repo_name}/"
    else:
        repo_url = ""
        site_url = ""

    return RepositoryContext(
        root=repo_root,
        github_handle=github_handle,
        repo_name=repo_name,
        repo_url=repo_url,
        site_url=site_url,
        src_package=detect_primary_package(repo_root),
        files=frozenset(files),
    )
resolve_repository_root
resolve_repository_root(root: Path | None = None) -> Path

Resolve the target repository root.

Source code in src/pup_core/inspect/detect.py
def resolve_repository_root(root: Path | None = None) -> Path:
    """Resolve the target repository root."""
    start = Path.cwd() if root is None else root
    start = start.expanduser().resolve()

    if not start.exists():
        raise RepositoryDetectionError(f"Repository path does not exist: {start}")

    if start.is_file():
        start = start.parent

    for candidate in (start, *start.parents):
        if (candidate / ".git").exists():
            return candidate

    return start
snapshot_repository_files
snapshot_repository_files(root: Path) -> set[str]

Return repository-relative file and directory markers.

Source code in src/pup_core/inspect/detect.py
def snapshot_repository_files(root: Path) -> set[str]:
    """Return repository-relative file and directory markers."""
    result: set[str] = set()

    ignored_dirs = {
        ".git",
        ".mypy_cache",
        ".pytest_cache",
        ".ruff_cache",
        ".venv",
        "__pycache__",
        "build",
        "dist",
        "htmlcov",
        "node_modules",
        "site",
    }

    for path in root.rglob("*"):
        relative = path.relative_to(root)

        if any(part in ignored_dirs for part in relative.parts):
            continue

        rel = relative.as_posix()

        if path.is_dir():
            result.add(rel)
            result.add(f"{rel}/")
        else:
            result.add(rel)

    return result

files

Repository file inspection.

find_files_by_suffix
find_files_by_suffix(
    root: Path, suffix: str
) -> tuple[Path, ...]

Return repository-relative files matching a suffix.

Source code in src/pup_core/inspect/files.py
def find_files_by_suffix(
    root: Path,
    suffix: str,
) -> tuple[Path, ...]:
    """Return repository-relative files matching a suffix."""
    return tuple(path for path in list_repository_files(root) if path.suffix == suffix)
is_python_source_path
is_python_source_path(path: Path) -> bool

Return whether a path represents authored Python source.

Source code in src/pup_core/inspect/files.py
def is_python_source_path(path: Path) -> bool:
    """Return whether a path represents authored Python source."""
    return path.suffix == ".py" and "__pycache__" not in path.parts
list_repository_files
list_repository_files(
    root: Path,
    *,
    ignored_directories: frozenset[
        str
    ] = _DEFAULT_IGNORED_DIRECTORIES,
) -> tuple[Path, ...]

Return authored repository files beneath a root.

Source code in src/pup_core/inspect/files.py
def list_repository_files(
    root: Path,
    *,
    ignored_directories: frozenset[str] = _DEFAULT_IGNORED_DIRECTORIES,
) -> tuple[Path, ...]:
    """Return authored repository files beneath a root."""
    files: list[Path] = []

    for path in root.rglob("*"):
        if not path.is_file():
            continue

        relative = path.relative_to(root)

        if any(part in ignored_directories for part in relative.parts):
            continue

        files.append(relative)

    return tuple(sorted(files))

git

Git repository inspection.

inspect_git
inspect_git(root: Path) -> GitInfo

Inspect Git facts without modifying the repository.

Source code in src/pup_core/inspect/git.py
def inspect_git(root: Path) -> GitInfo:
    """Inspect Git facts without modifying the repository."""
    origin_url = _run_git(root, "config", "--get", "remote.origin.url")
    github_owner, github_repo = _parse_github_remote(origin_url)

    tracked_files = _lines(_run_git(root, "ls-files"))
    untracked_files = _lines(
        _run_git(root, "ls-files", "--others", "--exclude-standard")
    )
    ignored_files = _lines(
        _run_git(
            root,
            "ls-files",
            "--others",
            "--ignored",
            "--exclude-standard",
        )
    )

    status = _run_git(root, "status", "--porcelain")

    return GitInfo(
        origin_url=origin_url,
        github_owner=github_owner,
        github_repo=github_repo,
        tracked_files=frozenset(tracked_files),
        untracked_files=frozenset(untracked_files),
        ignored_files=frozenset(ignored_files),
        dirty=bool(status.strip()),
    )

git_history

Read-only Git and GitHub file-history inspection.

local_last_changed
local_last_changed(
    *, repo_root: Path, path: str
) -> datetime | None

Return when a repository path was last changed in committed Git history.

Source code in src/pup_core/inspect/git_history.py
def local_last_changed(
    *,
    repo_root: Path,
    path: str,
) -> datetime | None:
    """Return when a repository path was last changed in committed Git history."""
    git_executable = shutil.which("git")

    if git_executable is None or not (repo_root / ".git").exists():
        return None

    result = subprocess.run(
        [
            git_executable,
            "log",
            "-1",
            "--format=%cI",
            "--",
            path,
        ],
        cwd=repo_root,
        capture_output=True,
        text=True,
        encoding="utf-8",
        check=False,
    )

    if result.returncode != 0:
        return None

    value = result.stdout.strip()

    if not value:
        return None

    try:
        return datetime.fromisoformat(value)
    except ValueError:
        return None
remote_last_changed
remote_last_changed(
    *, repository: str, path: str, ref: str
) -> datetime | None

Return when a file was last changed in a GitHub repository.

Source code in src/pup_core/inspect/git_history.py
def remote_last_changed(
    *,
    repository: str,
    path: str,
    ref: str,
) -> datetime | None:
    """Return when a file was last changed in a GitHub repository."""
    encoded_path = quote(path, safe="/")
    encoded_ref = quote(ref, safe="")

    url = (
        f"https://api.github.com/repos/{repository}/commits"
        f"?path={encoded_path}&sha={encoded_ref}&per_page=1"
    )

    headers = {
        "User-Agent": "pup-core",
        "Accept": "application/vnd.github+json",
    }

    token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")

    if token:
        headers["Authorization"] = f"Bearer {token}"

    request = Request(url, headers=headers)

    try:
        with urlopen(request, timeout=30) as response:
            payload = json.loads(response.read().decode("utf-8"))
    except HTTPError, URLError, json.JSONDecodeError:
        return None

    if not isinstance(payload, list) or not payload:
        return None

    first = payload[0]

    if not isinstance(first, dict):
        return None

    commit = first.get("commit")

    if not isinstance(commit, dict):
        return None

    committer = commit.get("committer")

    if not isinstance(committer, dict):
        return None

    value = committer.get("date")

    if not isinstance(value, str) or not value:
        return None

    try:
        return datetime.fromisoformat(value)
    except ValueError:
        return None

packages

Python package inspection.

detect_packages
detect_packages(root: Path) -> PackageInfo

Detect importable packages in a repository.

Source code in src/pup_core/inspect/packages.py
def detect_packages(root: Path) -> PackageInfo:
    """Detect importable packages in a repository."""
    src = root / "src"

    if src.is_dir():
        packages = _packages_under(src)
        return PackageInfo(
            src_layout=True,
            packages=packages,
            primary_package=_choose_primary_package(packages),
        )

    packages = _packages_under(root)

    return PackageInfo(
        src_layout=False,
        packages=packages,
        primary_package=_choose_primary_package(packages),
    )
detect_primary_package
detect_primary_package(root: Path) -> str

Return the primary detected Python package name.

Source code in src/pup_core/inspect/packages.py
def detect_primary_package(root: Path) -> str:
    """Return the primary detected Python package name."""
    return detect_packages(root).primary_package
module_exists
module_exists(root: Path, module_name: str) -> bool

Return whether a dotted Python module exists in the repository.

Source code in src/pup_core/inspect/packages.py
def module_exists(root: Path, module_name: str) -> bool:
    """Return whether a dotted Python module exists in the repository."""
    relative = Path(*module_name.split("."))

    for base in (root / "src", root):
        package_path = base / relative / "__init__.py"
        module_path = base / relative.with_suffix(".py")

        if package_path.is_file() or module_path.is_file():
            return True

    return False

pyproject

pyproject.toml inspection.

inspect_pyproject
inspect_pyproject(root: Path) -> PyprojectInfo

Return selected project and tooling facts from pyproject.toml.

Source code in src/pup_core/inspect/pyproject.py
def inspect_pyproject(root: Path) -> PyprojectInfo:
    """Return selected project and tooling facts from pyproject.toml."""
    data = load_pyproject(root)

    project = _mapping(data.get("project"))
    dependency_groups = _dependency_mapping(data.get("dependency-groups"))
    optional_dependencies = _dependency_mapping(project.get("optional-dependencies"))

    tool = _mapping(data.get("tool"))
    uv = _mapping(tool.get("uv"))
    ruff = _mapping(tool.get("ruff"))

    pytest = _mapping(tool.get("pytest"))
    pytest_ini = _mapping(pytest.get("ini_options"))

    hatch = _mapping(tool.get("hatch"))
    hatch_build = _mapping(hatch.get("build"))
    hatch_targets = _mapping(hatch_build.get("targets"))
    hatch_wheel = _mapping(hatch_targets.get("wheel"))
    hatch_hooks = _mapping(hatch_build.get("hooks"))
    hatch_vcs_hook = _mapping(hatch_hooks.get("vcs"))

    build_system = _mapping(data.get("build-system"))

    return PyprojectInfo(
        path=root / "pyproject.toml",
        project_name=_string(project.get("name")),
        requires_python=_string(project.get("requires-python")),
        dependencies=_string_tuple(project.get("dependencies")),
        dependency_groups=dependency_groups,
        optional_dependencies=optional_dependencies,
        scripts=_string_mapping(project.get("scripts")),
        build_backend=_string(build_system.get("build-backend")),
        uv_default_groups=_uv_default_groups(uv.get("default-groups")),
        ruff_target_version=_string(ruff.get("target-version")),
        pytest_testpaths=_string_tuple(pytest_ini.get("testpaths")),
        pytest_addopts=_string(pytest_ini.get("addopts")),
        hatch_version_file=_string(hatch_vcs_hook.get("version-file")),
        hatch_wheel_packages=_string_tuple(hatch_wheel.get("packages")),
    )
load_pyproject
load_pyproject(root: Path) -> dict[str, Any]

Load pyproject.toml without modifying it.

Source code in src/pup_core/inspect/pyproject.py
def load_pyproject(root: Path) -> dict[str, Any]:
    """Load pyproject.toml without modifying it."""
    path = root / "pyproject.toml"

    if not path.is_file():
        raise PyprojectError(f"pyproject.toml not found: {path}")

    try:
        with path.open("rb") as file:
            return tomllib.load(file)
    except tomllib.TOMLDecodeError as exc:
        raise PyprojectError(f"Invalid pyproject.toml: {path}") from exc

python_source

Python source-file inspection.

extract_module_docstring
extract_module_docstring(source: str) -> str | None

Return a Python module docstring from source text.

Source code in src/pup_core/inspect/python_source.py
def extract_module_docstring(source: str) -> str | None:
    """Return a Python module docstring from source text."""
    try:
        module = ast.parse(source)
    except SyntaxError:
        return None

    return ast.get_docstring(module, clean=True)
is_python_source_file
is_python_source_file(path: Path) -> bool

Return whether a path is a Python source file.

Source code in src/pup_core/inspect/python_source.py
def is_python_source_file(path: Path) -> bool:
    """Return whether a path is a Python source file."""
    return path.is_file() and path.suffix == ".py"
list_python_source_files
list_python_source_files(
    repository: RepositoryContext,
) -> tuple[Path, ...]

Return Python source files for a detected repository.

Source code in src/pup_core/inspect/python_source.py
def list_python_source_files(
    repository: RepositoryContext,
) -> tuple[Path, ...]:
    """Return Python source files for a detected repository."""
    root = repository.root

    if repository.src_package:
        package_root = root / "src" / repository.src_package

        if package_root.is_dir():
            return tuple(
                sorted(path for path in package_root.rglob("*.py") if path.is_file())
            )

    src_root = root / "src"

    if src_root.is_dir():
        return tuple(sorted(path for path in src_root.rglob("*.py") if path.is_file()))

    return tuple(sorted(path for path in root.glob("*.py") if path.is_file()))
read_module_docstring
read_module_docstring(path: Path) -> str | None

Return a Python module docstring from a source file.

Source code in src/pup_core/inspect/python_source.py
def read_module_docstring(path: Path) -> str | None:
    """Return a Python module docstring from a source file."""
    if not is_python_source_file(path):
        return None

    try:
        source = path.read_text(encoding="utf-8")
    except UnicodeDecodeError:
        return None

    return extract_module_docstring(source)

paths

Paths package.

normalize

Repository path normalization.

normalize_repo_path
normalize_repo_path(path: str | Path) -> str

Return a repository path using normalized POSIX separators.

Source code in src/pup_core/paths/normalize.py
def normalize_repo_path(path: str | Path) -> str:
    """Return a repository path using normalized POSIX separators."""
    value = str(path).replace("\\", "/")
    normalized = PurePosixPath(value)

    if str(normalized) == ".":
        return ""

    return normalized.as_posix()

safe

Safe repository-relative path handling.

safe_repo_path
safe_repo_path(
    root: Path, relative_path: str | Path
) -> Path

Resolve a repository-relative path without allowing escape.

Source code in src/pup_core/paths/safe.py
def safe_repo_path(root: Path, relative_path: str | Path) -> Path:
    """Resolve a repository-relative path without allowing escape."""
    root = root.resolve()
    candidate_input = Path(relative_path)

    if candidate_input.is_absolute():
        raise UnsafePathError(candidate_input)

    candidate = (root / candidate_input).resolve()

    try:
        candidate.relative_to(root)
    except ValueError as exc:
        raise UnsafePathError(candidate) from exc

    return candidate

python

Python package.

names

Python project and package name normalization.

canonical_distribution_name
canonical_distribution_name(name: str) -> str

Return the canonical comparison form of a distribution name.

Source code in src/pup_core/python/names.py
def canonical_distribution_name(name: str) -> str:
    """Return the canonical comparison form of a distribution name."""
    return _SEPARATOR_RE.sub("-", name).lower()
distribution_to_import_name
distribution_to_import_name(name: str) -> str

Convert a distribution name to its conventional import-name form.

Source code in src/pup_core/python/names.py
def distribution_to_import_name(name: str) -> str:
    """Convert a distribution name to its conventional import-name form."""
    return _SEPARATOR_RE.sub("_", name).lower()
normalize_import_name
normalize_import_name(name: str) -> str

Normalize a dotted Python import name for comparison.

Source code in src/pup_core/python/names.py
def normalize_import_name(name: str) -> str:
    """Normalize a dotted Python import name for comparison."""
    return ".".join(
        distribution_to_import_name(part) for part in name.strip().split(".") if part
    )

versions

Python version normalization.

minimum_python_version
minimum_python_version(requires_python: str) -> str

Extract the first major.minor version from requires-python.

Source code in src/pup_core/python/versions.py
def minimum_python_version(requires_python: str) -> str:
    """Extract the first major.minor version from requires-python."""
    return normalize_python_version(requires_python)
normalize_python_version
normalize_python_version(value: str) -> str

Normalize a Python version to major.minor form.

Source code in src/pup_core/python/versions.py
def normalize_python_version(value: str) -> str:
    """Normalize a Python version to major.minor form."""
    match = _VERSION_RE.search(value.strip())

    if match is None:
        return ""

    return f"{match.group('major')}.{match.group('minor')}"
python_version_to_ruff_target
python_version_to_ruff_target(version: str) -> str

Convert a Python major.minor version to Ruff target form.

Source code in src/pup_core/python/versions.py
def python_version_to_ruff_target(version: str) -> str:
    """Convert a Python major.minor version to Ruff target form."""
    normalized = normalize_python_version(version)

    if not normalized:
        return ""

    major, minor = normalized.split(".", maxsplit=1)
    return f"py{major}{minor}"
ruff_target_to_python_version
ruff_target_to_python_version(target: str) -> str

Convert a Ruff target version to Python major.minor form.

Source code in src/pup_core/python/versions.py
def ruff_target_to_python_version(target: str) -> str:
    """Convert a Ruff target version to Python major.minor form."""
    match = _RUFF_RE.fullmatch(target.strip().lower())

    if match is None:
        return ""

    return f"{match.group('major')}.{match.group('minor')}"

sections

Sections package.

apply

Managed section application.

apply_section_plan
apply_section_plan(text: str, plan: SectionPlan) -> str

Apply one previously planned managed-section operation.

Source code in src/pup_core/sections/apply.py
def apply_section_plan(text: str, plan: SectionPlan) -> str:
    """Apply one previously planned managed-section operation."""
    if plan.action == "unchanged":
        return text

    if plan.action == "delete":
        if plan.current is None:
            return text

        return text[: plan.current.start_index] + text[plan.current.end_index :]

    replacement = _render_section(plan)

    if plan.action == "replace":
        if plan.current is None:
            raise SectionError(f"Cannot replace missing managed section {plan.name!r}.")

        return (
            text[: plan.current.start_index]
            + replacement
            + text[plan.current.end_index :]
        )

    if plan.action == "add":
        if plan.current is not None:
            raise SectionError(f"Cannot add existing managed section {plan.name!r}.")

        if plan.insert_before_marker is not None:
            marker_count = text.count(plan.insert_before_marker)

            if marker_count != 1:
                raise SectionError(
                    f"Insertion marker for section {plan.name!r} "
                    "must occur exactly once."
                )

            index = text.index(plan.insert_before_marker)
            return text[:index] + replacement + text[index:]

        if not text:
            return replacement

        separator = "" if text.endswith("\n") else "\n"
        return text + separator + replacement

    raise SectionError(f"Unknown managed-section action: {plan.action!r}")

detect

Managed section detection.

find_managed_section
find_managed_section(
    text: str,
    *,
    name: str,
    start_marker: str,
    end_marker: str,
) -> ManagedSection | None

Find one explicitly bounded managed section.

Source code in src/pup_core/sections/detect.py
def find_managed_section(
    text: str,
    *,
    name: str,
    start_marker: str,
    end_marker: str,
) -> ManagedSection | None:
    """Find one explicitly bounded managed section."""
    start_count = text.count(start_marker)
    end_count = text.count(end_marker)

    if start_count == 0 and end_count == 0:
        return None

    if start_count != 1 or end_count != 1:
        raise SectionError(
            f"Managed section {name!r} must have exactly one "
            "start marker and one end marker."
        )

    start_index = text.index(start_marker)
    content_start = start_index + len(start_marker)
    end_marker_index = text.index(end_marker)

    if end_marker_index < content_start:
        raise SectionError(f"Managed section {name!r} has markers in the wrong order.")

    end_index = end_marker_index + len(end_marker)

    return ManagedSection(
        name=name,
        start_marker=start_marker,
        end_marker=end_marker,
        start_index=start_index,
        end_index=end_index,
        content_start=content_start,
        content_end=end_marker_index,
        content=text[content_start:end_marker_index],
    )

plan

Managed section planning.

plan_section_change
plan_section_change(
    text: str,
    *,
    name: str,
    start_marker: str,
    end_marker: str,
    desired_content: str | None,
    insert_before_marker: str | None = None,
) -> SectionPlan

Plan an add, replace, delete, or unchanged section operation.

Source code in src/pup_core/sections/plan.py
def plan_section_change(
    text: str,
    *,
    name: str,
    start_marker: str,
    end_marker: str,
    desired_content: str | None,
    insert_before_marker: str | None = None,
) -> SectionPlan:
    """Plan an add, replace, delete, or unchanged section operation."""
    current = find_managed_section(
        text,
        name=name,
        start_marker=start_marker,
        end_marker=end_marker,
    )

    if current is None:
        action = "unchanged" if desired_content is None else "add"
    elif desired_content is None:
        action = "delete"
    elif current.content == desired_content:
        action = "unchanged"
    else:
        action = "replace"

    return SectionPlan(
        name=name,
        action=action,
        start_marker=start_marker,
        end_marker=end_marker,
        current=current,
        desired_content=desired_content,
        insert_before_marker=insert_before_marker,
    )

templates

Shared canonical-template primitives.

baseline

Template-layer discovery and effective-file selection.

infer_layers
infer_layers(
    *, repo_root: Path, repo_name: str, files: set[str]
) -> list[str]

Infer additive template layers from repository structure.

Parameters:

Name Type Description Default
repo_root Path

Repository root.

required
repo_name str

Repository name.

required
files set[str]

Repository-relative file markers.

required

Returns:

Type Description
list[str]

Ordered additive template layers.

Source code in src/pup_core/templates/baseline.py
def infer_layers(
    *,
    repo_root: Path,
    repo_name: str,
    files: set[str],
) -> list[str]:
    """Infer additive template layers from repository structure.

    Args:
        repo_root: Repository root.
        repo_name: Repository name.
        files: Repository-relative file markers.

    Returns:
        Ordered additive template layers.
    """
    del repo_name

    has_py = "pyproject.toml" in files
    has_src = (repo_root / "src").is_dir()
    has_pypi = has_py and _is_pypi_project(repo_root)

    layers = ["ALL"]

    if has_py:
        layers.append("ALL-PY")

    if has_py and has_src:
        layers.append("ALL-PY-SRC")

    if has_py and has_src and has_pypi:
        layers.append("ALL-PY-SRC-PYPI")

    return layers
list_template_files
list_template_files(
    *,
    snapshot: TemplateSnapshot,
    layers: tuple[str, ...] | list[str],
) -> list[TemplateFile]

Return effective template files for selected additive layers.

Later layers override earlier layers for the same target path.

Source code in src/pup_core/templates/baseline.py
def list_template_files(
    *,
    snapshot: TemplateSnapshot,
    layers: tuple[str, ...] | list[str],
) -> list[TemplateFile]:
    """Return effective template files for selected additive layers.

    Later layers override earlier layers for the same target path.
    """
    by_target: dict[str, TemplateFile] = {}

    for layer in layers:
        layer_root = snapshot.root / layer

        if not layer_root.is_dir():
            continue

        for source_path in sorted(layer_root.rglob("*")):
            if not source_path.is_file():
                continue

            relative_path = source_path.relative_to(layer_root).as_posix()

            if _ignore_template_internal_file(relative_path):
                continue

            target_path = relative_path.removesuffix(".template")

            by_target[target_path] = TemplateFile(
                layer=layer,
                template_path=relative_path,
                target_path=target_path,
            )

    return sorted(
        by_target.values(),
        key=lambda item: item.target_path,
    )

fetch

Fetch canonical template snapshots without modifying target repositories.

fetch_template_snapshot
fetch_template_snapshot(
    *, source: TemplateSource
) -> Iterator[TemplateSnapshot]

Resolve a template source to one local immutable snapshot.

Source code in src/pup_core/templates/fetch.py
@contextmanager
def fetch_template_snapshot(
    *,
    source: TemplateSource,
) -> Iterator[TemplateSnapshot]:
    """Resolve a template source to one local immutable snapshot."""
    if source.local_path is not None:
        yield TemplateSnapshot(
            root=source.local_path.expanduser().resolve(),
            repository=source.repository,
            ref=source.ref,
            from_local=True,
        )
        return

    commit = resolve_ref_to_commit(
        repository=source.repository,
        ref=source.ref,
    )

    url = f"https://codeload.github.com/{source.repository}/tar.gz/{commit}"
    request = Request(url, headers=_github_headers())

    try:
        with urlopen(request, timeout=60) as response:
            archive_bytes = response.read()
    except (HTTPError, URLError) as exc:
        raise RuntimeError(f"Could not download template snapshot: {url}") from exc

    with TemporaryDirectory(prefix="pup-core-templates-") as raw_destination:
        destination = Path(raw_destination)

        try:
            with tarfile.open(
                fileobj=io.BytesIO(archive_bytes),
                mode="r:gz",
            ) as archive:
                archive.extractall(
                    path=destination,
                    filter="data",
                )
        except (tarfile.TarError, OSError) as exc:
            raise RuntimeError(f"Could not extract template snapshot: {url}") from exc

        directories = [entry for entry in destination.iterdir() if entry.is_dir()]

        if len(directories) != 1:
            raise RuntimeError(f"Unexpected template snapshot layout: {url}")

        yield TemplateSnapshot(
            root=directories[0],
            repository=source.repository,
            ref=commit,
            from_local=False,
        )
resolve_ref_to_commit
resolve_ref_to_commit(*, repository: str, ref: str) -> str

Resolve a GitHub branch or tag to an immutable commit SHA.

Source code in src/pup_core/templates/fetch.py
def resolve_ref_to_commit(
    *,
    repository: str,
    ref: str,
) -> str:
    """Resolve a GitHub branch or tag to an immutable commit SHA."""
    if _SHA_RE.fullmatch(ref):
        return ref

    url = f"https://api.github.com/repos/{repository}/commits/{quote(ref, safe='/')}"
    headers = _github_headers()
    headers["Accept"] = "application/vnd.github.sha"
    request = Request(url, headers=headers)

    try:
        with urlopen(request, timeout=30) as response:
            value = response.read().decode("utf-8").strip()
    except (HTTPError, URLError) as exc:
        raise RuntimeError(f"Could not resolve template ref: {url}") from exc

    if _SHA_RE.fullmatch(value):
        return value

    try:
        payload = json.loads(value)
    except json.JSONDecodeError as exc:
        raise RuntimeError(f"Unexpected GitHub ref response: {value!r}") from exc

    sha = payload.get("sha", "") if isinstance(payload, dict) else ""

    if not isinstance(sha, str) or not _SHA_RE.fullmatch(sha):
        raise RuntimeError(f"Unexpected GitHub ref response: {value!r}")

    return sha

render

Render canonical template content for a target repository.

read_rendered_template
read_rendered_template(
    *,
    snapshot: TemplateSnapshot,
    template_file: TemplateFile,
    repository: RepositoryContext,
) -> str

Read and render one canonical template file.

Source code in src/pup_core/templates/render.py
def read_rendered_template(
    *,
    snapshot: TemplateSnapshot,
    template_file: TemplateFile,
    repository: RepositoryContext,
) -> str:
    """Read and render one canonical template file."""
    source_path = snapshot.root / template_file.layer / template_file.template_path

    text = source_path.read_text(encoding="utf-8")

    return render_template(text, repository)
render_template
render_template(
    text: str, repository: RepositoryContext
) -> str

Render repository-specific values into template text.

Source code in src/pup_core/templates/render.py
def render_template(
    text: str,
    repository: RepositoryContext,
) -> str:
    """Render repository-specific values into template text."""
    replacements = {
        "repo_name": repository.repo_name,
        "github_handle": repository.github_handle,
        "repo_url": repository.repo_url,
        "site_url": repository.site_url,
        "src_package": repository.src_package,
    }

    rendered = text

    for name, value in replacements.items():
        rendered = rendered.replace(
            f"{{{{ {name} }}}}",
            value,
        )
        rendered = rendered.replace(
            f"{{{{{name}}}}}",
            value,
        )

    return rendered

types

Typed records for canonical template access.

TemplateFile dataclass

One effective file supplied by a template layer.

Source code in src/pup_core/templates/types.py
@dataclass(frozen=True)
class TemplateFile:
    """One effective file supplied by a template layer."""

    layer: str
    template_path: str
    target_path: str
TemplateSnapshot dataclass

Resolved canonical template snapshot.

Source code in src/pup_core/templates/types.py
@dataclass(frozen=True)
class TemplateSnapshot:
    """Resolved canonical template snapshot."""

    root: Path
    repository: str
    ref: str
    from_local: bool
TemplateSource dataclass

Location of the canonical template repository.

Source code in src/pup_core/templates/types.py
@dataclass(frozen=True)
class TemplateSource:
    """Location of the canonical template repository."""

    repository: str = "pup-pack/templates"
    ref: str = "main"
    local_path: Path | None = None

zensical

Zensical-specific template helpers.

merge_zensical_project_navigation
merge_zensical_project_navigation(
    *,
    template_data: Mapping[str, Any],
    repository_data: Mapping[str, Any],
) -> dict[str, Any]

Preserve repository-specific navigation in a canonical Zensical baseline.

The canonical template governs shared configuration. Repository-specific navigation remains local when present.

Source code in src/pup_core/templates/zensical.py
def merge_zensical_project_navigation(
    *,
    template_data: Mapping[str, Any],
    repository_data: Mapping[str, Any],
) -> dict[str, Any]:
    """Preserve repository-specific navigation in a canonical Zensical baseline.

    The canonical template governs shared configuration. Repository-specific
    navigation remains local when present.
    """
    merged = deepcopy(dict(template_data))

    template_project = _mapping(merged.get("project"))
    repository_project = _mapping(repository_data.get("project"))

    if "nav" in repository_project:
        template_project["nav"] = deepcopy(repository_project["nav"])

    merged["project"] = template_project

    return merged