Skip to content

API Reference

This page is auto-generated from Python docstrings.

pup_down

pup-down package.

__main__

Run pup-down as a module.

cli

Command-line interface for pup-down.

Compare the current repository infrastructure with its canonical templates.

Commands: uv run pup-down

Equivalent uvx usage after release: uvx pup-down uvx pup-down@latest

build_parser

build_parser() -> argparse.ArgumentParser

Build the argument parser.

Source code in src/pup_down/cli.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
def build_parser() -> argparse.ArgumentParser:
    """Build the argument parser."""
    parser = argparse.ArgumentParser(
        prog="pup-down",
        description=(
            "Compare the current repository infrastructure with the "
            "canonical templates and report template drift."
        ),
    )

    parser.add_argument(
        "--root",
        type=Path,
        default=None,
        help=(
            "Repository root to update. Defaults to the nearest parent "
            "directory containing .git, or the current directory."
        ),
    )
    parser.add_argument(
        "--templates",
        default="pup-pack/templates",
        help=(
            "GitHub owner/repo for canonical templates. Defaults to pup-pack/templates."
        ),
    )
    parser.add_argument(
        "--ref",
        default="main",
        help="Git ref, branch, or tag to fetch templates from. Defaults to main.",
    )
    parser.add_argument(
        "--templates-path",
        type=Path,
        default=None,
        help=(
            "Optional local templates repository path. If provided, templates "
            "are read from disk instead of GitHub raw URLs."
        ),
    )

    return parser

main

main(argv: Sequence[str] | None = None) -> int

Run the command-line interface.

Parameters:

Name Type Description Default
argv Sequence[str] | None

Optional command-line arguments. If None, uses sys.argv.

None

Returns:

Type Description
int

Exit code from the update command.

Source code in src/pup_down/cli.py
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
def main(argv: Sequence[str] | None = None) -> int:
    """Run the command-line interface.

    Args:
        argv: Optional command-line arguments. If None, uses sys.argv.

    Returns:
        Exit code from the update command.
    """
    parser = build_parser()
    args = parser.parse_args(argv)

    return update.run(
        root=args.root,
        templates=args.templates,
        ref=args.ref,
        templates_path=args.templates_path,
    )

commands

Command modules for pup-down.

Each command module exposes a stable run(...) -> int entry point.

The CLI parser lives in pup_down.cli. Behavior lives here.

update

Report repository scaffolding differences from the managed template baseline.

pup-down is read-only. It detects the target repository, resolves the effective template baseline from pup-core, compares the two, and prints a status line per managed file. Nothing on disk is modified.

run
run(
    *,
    root: Path | None = None,
    templates: str = "pup-pack/templates",
    ref: str = "main",
    templates_path: Path | None = None,
) -> int

Report repository changes that may belong in the templates.

Parameters:

Name Type Description Default
root Path | None

Repository root. If None, pup-down detects the current repo root.

None
templates str

GitHub owner/repo for canonical templates.

'pup-pack/templates'
ref str

Git ref, branch, or tag.

'main'
templates_path Path | None

Optional local templates repo path. When set, templates are read from disk instead of GitHub.

None

Returns:

Type Description
int

Process exit code. Always 0; pup-down reports and never fails on drift.

Raises:

Type Description
RepositoryDetectionError

If the target repository cannot be resolved.

UnsafePathError

If a template target path resolves outside the repo.

Source code in src/pup_down/commands/update.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
def run(
    *,
    root: Path | None = None,
    templates: str = "pup-pack/templates",
    ref: str = "main",
    templates_path: Path | None = None,
) -> int:
    """Report repository changes that may belong in the templates.

    Args:
        root: Repository root. If None, pup-down detects the current repo root.
        templates: GitHub owner/repo for canonical templates.
        ref: Git ref, branch, or tag.
        templates_path: Optional local templates repo path. When set, templates
            are read from disk instead of GitHub.

    Returns:
        Process exit code. Always 0; pup-down reports and never fails on drift.

    Raises:
        RepositoryDetectionError: If the target repository cannot be resolved.
        UnsafePathError: If a template target path resolves outside the repo.
    """
    repository = detect_repository(root)

    layers = tuple(
        infer_layers(
            repo_root=repository.root,
            repo_name=repository.repo_name,
            files=set(repository.files),
        )
    )

    source = TemplateSource(
        repository=templates,
        ref=ref,
        local_path=templates_path,
    )

    with fetch_template_snapshot(source=source) as snapshot:
        plan = compare_repository_to_template(
            repository=repository,
            layers=layers,
            snapshot=snapshot,
            template_repository=templates,
        )

    for file in plan.files:
        relative_path = file.path.relative_to(repository.root)

        status = file.status.upper().replace("-", " ")
        layer = f" [{file.source_layer}]" if file.source_layer else ""

        print(f"{status:<13} {relative_path}{layer}")

    return 0

compare

Compare current repository scaffolding with canonical templates.

This module holds the pup-down-specific comparison logic. Only the read-only classification of drift lives here.

compare_repository_to_template

compare_repository_to_template(
    *,
    repository: RepositoryContext,
    layers: tuple[str, ...],
    snapshot: TemplateSnapshot,
    template_repository: str,
) -> ComparisonPlan

Compare repository scaffolding with the effective template baseline.

For every effective template file, the repository copy is classified as current, missing (deleted in the repo), or differing. Differences are further split by last-change Git timestamps into repo-newer or template-newer, so drift that likely belongs back in the templates can be told apart from stale scaffolding.

Parameters:

Name Type Description Default
repository RepositoryContext

Detected repository context.

required
layers tuple[str, ...]

Effective additive template layers for the repository.

required
snapshot TemplateSnapshot

Resolved canonical template snapshot.

required
template_repository str

GitHub owner/repo for canonical templates.

required

Returns:

Type Description
ComparisonPlan

Complete read-only comparison plan.

Raises:

Type Description
UnsafePathError

If a template target path resolves outside the repository root.

Source code in src/pup_down/compare.py
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
def compare_repository_to_template(
    *,
    repository: RepositoryContext,
    layers: tuple[str, ...],
    snapshot: TemplateSnapshot,
    template_repository: str,
) -> ComparisonPlan:
    """Compare repository scaffolding with the effective template baseline.

    For every effective template file, the repository copy is classified as
    current, missing (deleted in the repo), or differing. Differences are
    further split by last-change Git timestamps into repo-newer or
    template-newer, so drift that likely belongs back in the templates can
    be told apart from stale scaffolding.

    Args:
        repository: Detected repository context.
        layers: Effective additive template layers for the repository.
        snapshot: Resolved canonical template snapshot.
        template_repository: GitHub owner/repo for canonical templates.

    Returns:
        Complete read-only comparison plan.

    Raises:
        UnsafePathError: If a template target path resolves outside the
            repository root.
    """
    template_files = list_template_files(
        snapshot=snapshot,
        layers=layers,
    )

    comparisons: list[ComparisonFile] = []

    for template_file in template_files:
        target_path = safe_repo_path(repository.root, template_file.target_path)

        template_text = read_rendered_template(
            snapshot=snapshot,
            template_file=template_file,
            repository=repository,
        )

        if not target_path.exists():
            comparisons.append(
                ComparisonFile(
                    path=target_path,
                    status="deleted-in-repo",
                    source_layer=template_file.layer,
                    source_path=template_file.template_path,
                    repo_text=None,
                    template_text=template_text,
                )
            )
            continue

        if not target_path.is_file():
            continue

        try:
            repo_text = target_path.read_text(encoding="utf-8")
        except UnicodeDecodeError:
            continue

        if repo_text == template_text:
            status: FileStatus = "current"
            repo_changed = None
            template_changed = None
        else:
            repo_changed = local_last_changed(
                repo_root=repository.root,
                path=template_file.target_path,
            )

            template_source_path = (
                f"{template_file.layer}/{template_file.template_path}"
            )

            if snapshot.from_local:
                template_changed = local_last_changed(
                    repo_root=snapshot.root,
                    path=template_source_path,
                )
            else:
                template_changed = remote_last_changed(
                    repository=template_repository,
                    path=template_source_path,
                    ref=snapshot.ref,
                )

            status = _classify_difference(
                repo_changed=repo_changed,
                template_changed=template_changed,
            )

        comparisons.append(
            ComparisonFile(
                path=target_path,
                status=status,
                source_layer=template_file.layer,
                source_path=template_file.template_path,
                repo_text=repo_text,
                template_text=template_text,
                repo_changed=repo_changed,
                template_changed=template_changed,
            )
        )

    return ComparisonPlan(
        target=repository,
        layers=layers,
        files=tuple(comparisons),
    )