Skip to content

Commit ea2c112

Browse files
committed
Added improved release script
Signed-off-by: Nick Brook <[email protected]>
1 parent 879b2b2 commit ea2c112

4 files changed

Lines changed: 232 additions & 15 deletions

File tree

.pre-commit-config.yaml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,19 @@ repos:
1818
rev: v1.15.0
1919
hooks:
2020
- id: mypy
21-
exclude: ^(examples)
21+
exclude: ^(examples|scripts)
2222
args: [
2323
--config-file, pyproject.toml,
24-
2524
]
2625
additional_dependencies: [
2726
bleak==0.22.3,
2827
rich==13.9.4,
2928
packaging,
3029
prompt_toolkit>=3.0.0,
3130
]
31+
32+
- repo: https://github.com/PyCQA/bandit
33+
rev: '1.8.3'
34+
hooks:
35+
- id: bandit
36+
args: [ -c, pyproject.toml, "-r", "."]

Makefile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,14 @@ ifdef USE_UV
119119
else
120120
pip install -e ".[dev]"
121121
endif
122+
123+
# If the first argument is "run"...
124+
ifeq (release,$(firstword $(MAKECMDGOALS)))
125+
# use the rest as arguments for "run"
126+
RUN_ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
127+
# ...and turn them into do-nothing targets
128+
$(eval $(RUN_ARGS):;@:)
129+
endif
130+
131+
release: ## release the package
132+
$(PY_CMD_PREFIX) python scripts/release.py $(RUN_ARGS)

pyproject.toml

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -162,28 +162,20 @@ skip_missing_interpreters = false
162162
runner = "uv-venv-lock-runner"
163163
description = "Run test under {base_python}"
164164
dependency_groups = ["test"]
165-
commands = [["pytest"]]
165+
commands = [["make", "test"]]
166+
allowlist_externals = ["make"]
166167

167168
[tool.tox.env.lint]
168-
runner = "uv-venv-lock-runner"
169169
description = "format and lint code"
170170
dependency_groups = ["lint"]
171-
commands = [
172-
["ruff", "check", "."],
173-
["ruff", "format", "."],
174-
]
171+
commands = [["make", "lint"]]
175172

176173
[tool.tox.env.format]
177-
runner = "uv-venv-lock-runner"
178174
description = "format code"
179175
dependency_groups = ["lint"]
180-
commands = [
181-
["ruff", "format", "."],
182-
["ruff", "check", "--select", "I", "--fix", "."],
183-
]
176+
commands = [["make", "format"]]
184177

185178
[tool.tox.env.type]
186-
runner = "uv-venv-lock-runner"
187179
description = "type check code"
188180
dependency_groups = ["type"]
189-
commands = [["mypy"]]
181+
commands = [["make", "typecheck"]]

scripts/release.py

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
#!/usr/bin/env python3
2+
"""Script to bump the version of the package.
3+
4+
Usage: python scripts/bump_version.py [major|minor|patch]
5+
"""
6+
7+
import argparse
8+
import re
9+
import subprocess
10+
import sys
11+
from datetime import datetime
12+
from pathlib import Path
13+
14+
15+
def update_pyproject_toml(new_version):
16+
"""Update the version in pyproject.toml."""
17+
pyproject_path = Path(__file__).parent.parent / "pyproject.toml"
18+
content = pyproject_path.read_text()
19+
20+
# Replace the version in project section
21+
content = re.sub(
22+
r'version = "[0-9]+\.[0-9]+\.[0-9]+"',
23+
f'version = "{new_version}"',
24+
content,
25+
)
26+
27+
pyproject_path.write_text(content)
28+
print(f"Updated version in pyproject.toml to {new_version}")
29+
30+
31+
def update_init_py(new_version):
32+
"""Update the version in __init__.py."""
33+
init_py_path = Path(__file__).parent.parent / "test_a_ble" / "__init__.py"
34+
content = init_py_path.read_text()
35+
36+
# Replace the version
37+
content = re.sub(
38+
r'__version__ = "[0-9]+\.[0-9]+\.[0-9]+"',
39+
f'__version__ = "{new_version}"',
40+
content,
41+
)
42+
43+
init_py_path.write_text(content)
44+
print(f"Updated version in __init__.py to {new_version}")
45+
46+
47+
def update_docs_conf_py(new_version):
48+
"""Update the version in docs/source/conf.py."""
49+
conf_py_path = Path(__file__).parent.parent / "docs" / "source" / "conf.py"
50+
content = conf_py_path.read_text()
51+
52+
# Replace the version
53+
content = re.sub(r"release = '[0-9]+\.[0-9]+\.[0-9]+'", f"release = '{new_version}'", content)
54+
55+
conf_py_path.write_text(content)
56+
print(f"Updated version in docs/source/conf.py to {new_version}")
57+
58+
59+
def update_changelog(new_version):
60+
"""Update the changelog with a new version section."""
61+
changelog_path = Path(__file__).parent.parent / "CHANGELOG.md"
62+
content = changelog_path.read_text()
63+
64+
# Check if the new version already exists in the changelog
65+
if f"## [{new_version}]" in content:
66+
print(f"Version {new_version} already exists in CHANGELOG.md")
67+
return
68+
69+
# Get the current date
70+
today = datetime.now().strftime("%Y-%m-%d")
71+
72+
# Create a new version section
73+
new_section = f"""## [{new_version}] - {today}
74+
75+
### Added
76+
-
77+
78+
### Changed
79+
-
80+
81+
### Fixed
82+
-"""
83+
84+
# Find the position after the header section
85+
header_end = content.find("## [")
86+
if header_end == -1:
87+
print("Could not find version section in CHANGELOG.md")
88+
return
89+
90+
# Insert the new section after the header
91+
updated_content = content[:header_end] + new_section + "\n\n\n" + content[header_end:]
92+
93+
changelog_path.write_text(updated_content)
94+
print(f"Updated CHANGELOG.md with new version {new_version}")
95+
96+
97+
def get_current_version():
98+
"""Get the current version from pyproject.toml."""
99+
pyproject_path = Path(__file__).parent.parent / "pyproject.toml"
100+
try:
101+
content = pyproject_path.read_text()
102+
match = re.search(r'^version = "([0-9]+\.[0-9]+\.[0-9]+)"', content, re.MULTILINE)
103+
if match:
104+
return match.group(1)
105+
except Exception as e:
106+
print(f"Error reading pyproject.toml: {e}")
107+
sys.exit(1)
108+
109+
print("Could not find version in pyproject.toml")
110+
sys.exit(1)
111+
112+
113+
def bump_version(current_version, part):
114+
"""Bump the version according to the specified part."""
115+
major, minor, patch = map(int, current_version.split("."))
116+
117+
if part == "major":
118+
major += 1
119+
minor = 0
120+
patch = 0
121+
elif part == "minor":
122+
minor += 1
123+
patch = 0
124+
elif part == "patch":
125+
patch += 1
126+
else:
127+
print(f"Invalid part: {part}")
128+
sys.exit(1)
129+
130+
return f"{major}.{minor}.{patch}"
131+
132+
133+
def run_command(cmd, check=True):
134+
"""Run a command safely."""
135+
try:
136+
return subprocess.run(cmd, check=check, shell=False, text=True, capture_output=True) # noqa: S603 # nosec: B603
137+
except subprocess.CalledProcessError as e:
138+
print(f"Command failed: {e}")
139+
print(f"Command output:\n{e.stdout}\n{e.stderr}")
140+
sys.exit(1)
141+
142+
143+
def run_checks():
144+
"""Run the checks for the release."""
145+
print("Running checks...")
146+
run_command(["make", "check"])
147+
print("Checks passed.")
148+
149+
150+
def run_git_command(cmd, check=True):
151+
"""Run a git command safely."""
152+
return run_command(["git", *cmd], check=check)
153+
154+
155+
def main():
156+
"""Execute the main function."""
157+
parser = argparse.ArgumentParser(description="Bump the version of the package.")
158+
parser.add_argument(
159+
"part",
160+
nargs="?",
161+
choices=["major", "minor", "patch"],
162+
help="The part of the version to bump (optional)",
163+
)
164+
args = parser.parse_args()
165+
166+
current_version = get_current_version()
167+
if args.part:
168+
new_version = bump_version(current_version, args.part)
169+
print(f"Bumping version from {current_version} to {new_version}")
170+
171+
update_pyproject_toml(new_version)
172+
update_init_py(new_version)
173+
update_docs_conf_py(new_version)
174+
update_changelog(new_version)
175+
run_checks()
176+
177+
print(f"Version bumped to {new_version}")
178+
print("Now update the changelog: CHANGELOG.md")
179+
print("Don't forget to commit the changes and create a new tag:")
180+
print(f"git commit -am 'Bump version to {new_version}'")
181+
else:
182+
new_version = current_version
183+
print("Commands to run to create a new tag:")
184+
185+
print(f"git tag -a v{new_version} -m 'Version {new_version}'")
186+
print("git push && git push --tags")
187+
print("\nDo you want to run the git commands now? (y/n)")
188+
response = input().strip().lower()
189+
190+
if response in {"y", "yes"}:
191+
print("Running git commands...")
192+
if args.part:
193+
run_git_command(["commit", "-am", f"Bump version to {new_version}"])
194+
run_git_command(["tag", "-a", f"v{new_version}", "-m", f"Version {new_version}"])
195+
196+
print("Do you want to push the changes? (y/n)")
197+
push_response = input().strip().lower()
198+
if push_response in {"y", "yes"}:
199+
run_git_command(["push"])
200+
run_git_command(["push", "--tags"])
201+
print("Changes pushed successfully.")
202+
else:
203+
print("Changes committed and tagged locally. Run 'git push && git push --tags' when ready.")
204+
else:
205+
print("Commands not executed. Run them manually when ready.")
206+
207+
208+
if __name__ == "__main__":
209+
main()

0 commit comments

Comments
 (0)