cli_release-me 1.1.3__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ Metadata-Version: 2.4
2
+ Name: cli_release-me
3
+ Version: 1.1.3
4
+ Summary: CLI tool for creating git-tagged versions of Python packages where the version has to be specified exactly once.
5
+ Author-email: Thomas Bauwens <thomas.bauwens@kuleuven.be>
@@ -0,0 +1,6 @@
1
+ releaseme/__init__.py,sha256=gSBHnsgfhBqsPhGkuT3ufUcOaqBATscOtwhSyDd-_90,22
2
+ releaseme/_cli.py,sha256=TADK_cW8dNcytfFxmKLlK7wmpiDN2lEbakxDe-7Gu9E,8163
3
+ cli_release_me-1.1.3.dist-info/METADATA,sha256=XvYrY1hXQRUZX3CH0oCtLermkSITtYh60V020kdoylw,238
4
+ cli_release_me-1.1.3.dist-info/WHEEL,sha256=tkmg4JIqwd9H8mL30xA7crRmoStyCtGp0VWshokd1Jc,105
5
+ cli_release_me-1.1.3.dist-info/entry_points.txt,sha256=SW2fy_zRVDphVPZUdzU2V6qqCx-LuTnT4ZyJ_nmTDeI,51
6
+ cli_release_me-1.1.3.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py2-none-any
5
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ releaseme = releaseme._cli:_main
releaseme/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "v1.1.3"
releaseme/_cli.py ADDED
@@ -0,0 +1,180 @@
1
+ #!/usr/bin/env python3
2
+
3
+
4
+ def _main():
5
+ import argparse
6
+ import os
7
+ import re
8
+ import sys
9
+ import tomllib
10
+ import subprocess
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+ print() # Newline
15
+
16
+ # Define arguments. They are only parsed after we print the current version.
17
+ parser = argparse.ArgumentParser(description="Push a new tagged version of a Python package.")
18
+ parser.add_argument("version", type=str, help="New version number.")
19
+ parser.add_argument("--runtime_variable_path", type=Path, help="Path to the file where the version is defined in a variable.")
20
+ parser.add_argument("--runtime_variable_name", type=str, help="Name of the variable whose value should be set to the current version.", default="__version__")
21
+
22
+ # Sanity check: are we even in a Python package tracked by Git?
23
+ PATH_GIT = Path(".git")
24
+ PATH_TOML = Path("pyproject.toml")
25
+ if not PATH_GIT.exists() or not PATH_TOML.exists():
26
+ print("❌ This does not look like a Python project root.")
27
+ sys.exit(1)
28
+
29
+ # Inspect the package for its name and version.
30
+ # - The TOML definitely exists. Question is whether it is correctly formed.
31
+ def get_distribution_name() -> str:
32
+ with open(PATH_TOML, "rb") as handle:
33
+ try:
34
+ return tomllib.load(handle)["project"]["name"]
35
+ except:
36
+ print("❌ Missing project name in TOML.")
37
+ sys.exit(1)
38
+
39
+ DISTRIBUTION_NAME = get_distribution_name()
40
+ print(f"✅ Identified distribution: {DISTRIBUTION_NAME}")
41
+
42
+ # - And even with a project name, can we find the source code?
43
+ def get_package_path() -> Path:
44
+ with open(PATH_TOML, "rb") as handle:
45
+ try: # This is most specific and hence has precedent.
46
+ package = Path(tomllib.load(handle)["tool.hatch.build.targets.wheel"]["packages"][0])
47
+ except:
48
+ # If there is a ./src/, it is always investigated.
49
+ parent_of_package = Path("./src/")
50
+ if not parent_of_package.is_dir():
51
+ parent_of_package = parent_of_package.parent
52
+
53
+ # Now, if there is a folder here with the same name as the distribution, that has to be it.
54
+ _, subfolders, _ = next(os.walk(parent_of_package))
55
+ subfolders = [f for f in subfolders if not f.startswith(".") and not f.startswith("_") and not f.endswith(".egg-info")]
56
+
57
+ if DISTRIBUTION_NAME in subfolders:
58
+ package = parent_of_package / DISTRIBUTION_NAME
59
+ # Or, if there is only one subfolder, that's likely it.
60
+ elif len(subfolders) == 1:
61
+ package = parent_of_package / subfolders[0]
62
+ else:
63
+ print("❌ Could not find package name.")
64
+ sys.exit(1)
65
+
66
+ # Verify that this folder contains an __init__.py as a sanity check that it is actually a Python module.
67
+ if not (package / "__init__.py").is_file():
68
+ print(f"❌ Missing __init__.py in supposed package root {package.as_posix()}!")
69
+ sys.exit(1)
70
+
71
+ return package
72
+
73
+ def get_package_name() -> str:
74
+ return get_package_path().name
75
+
76
+ PACKAGE_NAME = get_package_name()
77
+ print(f"✅ Identified package: {PACKAGE_NAME}")
78
+
79
+ # - Can we find the old and new tags?
80
+ def get_last_version_tag() -> Optional[str]:
81
+ try:
82
+ return subprocess.check_output(["git", "describe", "--tags", "--abbrev=0"], text=True).strip()
83
+ except subprocess.CalledProcessError:
84
+ return None
85
+
86
+ def is_numeric_version_tag(version: str) -> bool:
87
+ return re.match(r"^v?[0-9.]+$", version) is not None
88
+
89
+ def is_version_lower(v1: str, v2: str):
90
+ return tuple(int(p) for p in v1.removeprefix("v").split(".")) < tuple(int(p) for p in v2.removeprefix("v").split("."))
91
+
92
+ CURRENT_VERSION = get_last_version_tag()
93
+ if CURRENT_VERSION is not None:
94
+ print(f"✅ Identified current version: {CURRENT_VERSION}")
95
+
96
+ args = parser.parse_args()
97
+ NEW_VERSION = args.version.strip()
98
+ if CURRENT_VERSION is not None:
99
+ if is_numeric_version_tag(CURRENT_VERSION) and is_numeric_version_tag(NEW_VERSION): # These checks are immune to a 'v' prefix.
100
+ if is_version_lower(NEW_VERSION, CURRENT_VERSION): # Idem.
101
+ print(f"❌ Cannot use new version {NEW_VERSION} since it is lower than the current version.")
102
+ sys.exit(1)
103
+ if CURRENT_VERSION.startswith("v") and not NEW_VERSION.startswith("v"):
104
+ NEW_VERSION = "v" + NEW_VERSION
105
+ else:
106
+ if is_numeric_version_tag(NEW_VERSION) and not NEW_VERSION.startswith("v"):
107
+ NEW_VERSION = "v" + NEW_VERSION
108
+
109
+ # Summarise the commits since the last tag.
110
+ def generate_release_notes(from_tag: Optional[str]):
111
+ if not from_tag:
112
+ print("⚠️ No previous tag found, listing all commits")
113
+ range_spec = "--all"
114
+ else:
115
+ range_spec = f"{from_tag}..HEAD"
116
+
117
+ sep = "<<END>>"
118
+ log = subprocess.check_output(["git", "log", range_spec, f"--pretty=format:%B{sep}"], text=True).strip()
119
+ if not log:
120
+ print(f"❌ No changes were made since the last version ({CURRENT_VERSION})!")
121
+ sys.exit(1)
122
+
123
+ commit_titles = [s.strip().split("\n")[0] for s in log.split(sep)]
124
+ return "".join("- " + title + "\n"
125
+ for title in commit_titles if title)
126
+
127
+ def quote(s: str) -> str:
128
+ return "\n".join(" | " + line for line in [""] + s.strip().split("\n") + [""])
129
+
130
+ notes = generate_release_notes(CURRENT_VERSION)
131
+ print(f"✅ Generated release notes since {CURRENT_VERSION or 'initial commit'}:")
132
+ print(quote(notes))
133
+
134
+ # Update all mentions of the version in the project files.
135
+ def update_pyproject(version: str):
136
+ content = PATH_TOML.read_text()
137
+ new_content = re.sub(r"""version\s*=\s*["'][0-9a-zA-Z.\-+]+["']""", f'version = "{version}"', content)
138
+ PATH_TOML.write_text(new_content)
139
+ print(f"✅ Updated pyproject.toml to version {version}")
140
+
141
+ PATH_VARIABLE = args.runtime_variable_path or get_package_path() / "__init__.py"
142
+ def update_variable(version: str):
143
+ if not PATH_VARIABLE.exists():
144
+ print(f"⚠️ {PATH_VARIABLE.name} not found; skipping {args.runtime_variable_name} update")
145
+ return
146
+ content = PATH_VARIABLE.read_text()
147
+ new_content = re.sub(re.escape(args.runtime_variable_name) + r"""\s*=\s*["'][0-9a-zA-Z.\-+]+["']""",
148
+ f'{args.runtime_variable_name} = "{version}"', content)
149
+ PATH_VARIABLE.write_text(new_content)
150
+ print(f"✅ Updated {PATH_VARIABLE.name} to version {version}")
151
+
152
+ if input(f"⚠️ Please confirm that you want to release the above details as follows:\n 📦 Package: {PACKAGE_NAME}\n ⏳ Version: {NEW_VERSION}\n 🌐 PyPI: {DISTRIBUTION_NAME}\n([y]/n) ").lower() == "n":
153
+ print(f"❌ User abort.")
154
+ sys.exit(1)
155
+
156
+ update_pyproject(NEW_VERSION)
157
+ update_variable(NEW_VERSION)
158
+
159
+ # Save changes with Git.
160
+ def git_commit_tag_push(version: str, notes: str):
161
+ try:
162
+ print(quote(
163
+ subprocess.check_output(["git", "add", "pyproject.toml", PATH_VARIABLE.as_posix()], text=True) + \
164
+ subprocess.check_output(["git", "commit", "-m", f"🔖 Release {version}\n\n{notes}"], text=True) + \
165
+ subprocess.check_output(["git", "push"], text=True)
166
+ ))
167
+ print(quote(
168
+ subprocess.check_output(["git", "tag", "-a", f"{version}", "-m", f"Release {version}\n\n{notes}"], text=True) + \
169
+ subprocess.check_output(["git", "push", "origin", f"{version}"], text=True)
170
+ ))
171
+ except:
172
+ print(f"❌ Failed to save to Git.")
173
+ raise
174
+ print(f"✅ Committed, tagged, and pushed version {version} with release notes.")
175
+
176
+ git_commit_tag_push(NEW_VERSION, notes)
177
+
178
+
179
+ if __name__ == "__main__": # Run from command line.
180
+ _main()