File: util.py

package info (click to toggle)
python-maison 2.0.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 816 kB
  • sloc: python: 1,549; makefile: 9; sh: 5
file content (166 lines) | stat: -rw-r--r-- 5,099 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
"""Module containing util."""

import argparse
import stat
import subprocess
from pathlib import Path
from typing import Any
from typing import Callable
from typing import Optional


REPO_FOLDER: Path = Path(__file__).resolve().parent.parent


class MissingDependencyError(Exception):
    """Exception raised when a depedency is missing from the system running setup-repo."""

    def __init__(self, project: Path, dependency: str):
        """Initializes MisssingDependencyError."""
        message_lines: list[str] = [
            f"Unable to find {dependency=}.",
            f"Please ensure that {dependency} is installed before setting up the repo at {project.absolute()}",
        ]
        message: str = "\n".join(message_lines)
        super().__init__(message)


def check_dependencies(path: Path, dependencies: list[str]) -> None:
    """Checks for any passed dependencies."""
    for dependency in dependencies:
        try:
            subprocess.check_call([dependency, "--version"], cwd=path)
        except subprocess.CalledProcessError as e:
            raise MissingDependencyError(path, dependency) from e


def existing_dir(value: str) -> Path:
    """Responsible for validating argparse inputs and returning them as pathlib Path's if they meet criteria."""
    path = Path(value).expanduser().resolve()

    if not path.exists():
        raise argparse.ArgumentTypeError(f"{path} does not exist.")
    if not path.is_dir():
        raise argparse.ArgumentTypeError(f"{path} is not a directory.")

    return path


def remove_readonly(func: Callable[[str], Any], path: str, _: Any) -> None:
    """Clears the readonly bit and attempts to call the provided function.

    This is passed to shutil.rmtree as the onerror kwarg.
    """
    Path(path).chmod(stat.S_IWRITE)
    func(path)


def get_package_version() -> str:
    """Gets the package version."""
    result: subprocess.CompletedProcess = subprocess.run(
        ["uvx", "--from", "commitizen", "cz", "version", "-p"],
        cwd=REPO_FOLDER,
        capture_output=True,
    )
    return result.stdout.decode("utf-8").strip()


def get_bumped_package_version(increment: Optional[str] = None) -> str:
    """Gets the bumped package version."""
    args: list[str] = [
        "uvx",
        "--from",
        "commitizen",
        "cz",
        "bump",
        "--get-next",
        "--yes",
        "--dry-run",
    ]
    if increment is not None:
        args.extend(["--increment", increment])
    result: subprocess.CompletedProcess = subprocess.run(
        args, cwd=REPO_FOLDER, capture_output=True
    )
    return result.stdout.decode("utf-8").strip()


def create_release_branch(new_version: str) -> None:
    """Creates a release branch."""
    commands: list[list[str]] = [
        ["git", "status", "--porcelain"],
        ["git", "checkout", "-b", f"release/{new_version}", "main"],
    ]
    for command in commands:
        subprocess.run(command, cwd=REPO_FOLDER, capture_output=True, check=True)


def bump_version(increment: Optional[str] = None) -> None:
    """Bumps the package version."""
    bump_cmd: list[str] = [
        "uvx",
        "--from",
        "commitizen",
        "cz",
        "bump",
        "--yes",
        "--files-only",
        "--changelog",
    ]
    if increment is not None:
        bump_cmd.extend(["--increment", increment])
    subprocess.run(bump_cmd, cwd=REPO_FOLDER, check=True)


def get_latest_tag() -> Optional[str]:
    """Gets the latest git tag."""
    sort_tags: list[str] = ["git", "tag", "--sort=-creatordate"]
    find_last: list[str] = ["grep", "-v", '"${GITHUB_REF_NAME}"']
    echo_none: list[str] = ["echo", "''"]
    result: subprocess.CompletedProcess = subprocess.run(
        [*sort_tags, "|", *find_last, "|", "tail", "-n1", "||", *echo_none],
        cwd=REPO_FOLDER,
        capture_output=True,
    )
    tag: str = result.stdout.decode("utf-8").strip()
    if tag == "":
        return None
    return tag


def get_latest_release_notes() -> str:
    """Gets the release notes.

    Assumes the latest_tag hasn't been applied yet.
    """
    latest_tag: Optional[str] = get_latest_tag()
    latest_version: str = get_package_version()
    if latest_tag == latest_version:
        raise ValueError(
            "The latest tag and version are the same. Please ensure the release notes are taken before tagging."
        )
    rev_range: str = "" if latest_tag is None else f"{latest_tag}..{latest_version}"
    command: list[str] = [
        "uvx",
        "--from",
        "commitizen",
        "cz",
        "changelog",
        rev_range,
        "--dry-run",
        "--unreleased-version",
        latest_version,
    ]
    result: subprocess.CompletedProcess = subprocess.run(
        command, cwd=REPO_FOLDER, capture_output=True, check=True
    )
    return result.stdout.decode("utf-8")


def tag_release() -> None:
    """Tags the release using commitizen bump with tag only."""
    subprocess.run(
        ["uvx", "--from", "commitizen", "cz", "bump", "--tag-only", "--yes"],
        cwd=REPO_FOLDER,
        check=True,
    )