Skip to content

Commit 6d9ba4d

Browse files
committed
pre-commit: Add hook to ensure version consistency in source tree
1 parent f6aa66b commit 6d9ba4d

File tree

2 files changed

+149
-0
lines changed

2 files changed

+149
-0
lines changed

.pre-commit-config.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,17 @@ repos:
1212
rev: stable
1313
hooks:
1414
- id: black
15+
- repo: local
16+
hooks:
17+
- id: version-check
18+
name: version-check
19+
description: "Check if version is consistent in all source files"
20+
entry: .pre-commit/version_check.py
21+
pass_filenames: false
22+
stages:
23+
- commit
24+
- manual
25+
language: python
26+
files: ^(\.pre-commit/version_check\.py|setup\.py|docs/conf\.py|docs/changelog\.rst)$
27+
additional_dependencies:
28+
- sphinx

.pre-commit/version_check.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import os
2+
import re
3+
import runpy
4+
import subprocess
5+
import sys
6+
7+
import docutils.nodes
8+
import docutils.parsers.rst
9+
import docutils.utils
10+
import docutils.frontend
11+
12+
13+
def parse_rst(text: str) -> docutils.nodes.document:
14+
parser = docutils.parsers.rst.Parser()
15+
components = (docutils.parsers.rst.Parser,)
16+
settings = docutils.frontend.OptionParser(
17+
components=components
18+
).get_default_values()
19+
document = docutils.utils.new_document("<rst-doc>", settings=settings)
20+
parser.parse(text, document)
21+
return document
22+
23+
24+
class SectionVisitor(docutils.nodes.NodeVisitor):
25+
def __init__(self, *args, **kwargs):
26+
super().__init__(*args, **kwargs)
27+
self.sectiontitles_found = []
28+
29+
def visit_section(self, node: docutils.nodes.section) -> None:
30+
"""Called for "section" nodes."""
31+
title = node[0]
32+
assert isinstance(title, docutils.nodes.title)
33+
self.sectiontitles_found.append(title.astext())
34+
35+
def unknown_visit(self, node: docutils.nodes.Node) -> None:
36+
"""Called for all other node types."""
37+
pass
38+
39+
40+
def get_sphinxchangelog_version(rootdir):
41+
with open(os.path.join(rootdir, "docs", "changelog.rst"), mode="r") as f:
42+
doc = parse_rst(f.read())
43+
44+
visitor = SectionVisitor(doc)
45+
doc.walk(visitor)
46+
47+
unique_sectiontitles = set(visitor.sectiontitles_found)
48+
assert len(visitor.sectiontitles_found) == len(unique_sectiontitles)
49+
assert visitor.sectiontitles_found[0] == "Changelog"
50+
51+
changelog_pattern = re.compile(r"^Version (\S+)((?: \(unreleased\)))?$")
52+
53+
matchobj = changelog_pattern(visitor.sectiontitles_found[1])
54+
assert matchobj
55+
version = matchobj.group(1)
56+
version_unreleased = matchobj.group(2)
57+
58+
matchobj = changelog_pattern(visitor.sectiontitles_found[2])
59+
assert matchobj
60+
release = matchobj.group(1)
61+
release_unreleased = matchobj.group(2)
62+
63+
if version_unreleased:
64+
assert release_unreleased
65+
66+
return version, release
67+
68+
69+
def get_sphinxconfpy_version(rootdir):
70+
"""Get version from Sphinx' conf.py."""
71+
sphinx_conf = runpy.run_path(os.path.join(rootdir, "docs", "conf.py"))
72+
version, sep, bugfix = sphinx_conf["release"].rpartition(".")
73+
assert sep == "."
74+
assert bugfix
75+
assert version == sphinx_conf["version"]
76+
return sphinx_conf["version"], sphinx_conf["release"]
77+
78+
79+
def get_setuppy_version(rootdir):
80+
"""Get version from setup.py."""
81+
setupfile = os.path.join(rootdir, "setup.py")
82+
cmd = (sys.executable, setupfile, "--version")
83+
release = subprocess.check_output(cmd, text=True).rstrip("\n")
84+
version = release.rpartition(".")[0]
85+
return version, release
86+
87+
88+
def main():
89+
rootdir = os.path.join(os.path.dirname(__file__), "..")
90+
91+
setuppy_version, setuppy_release = get_setuppy_version(rootdir)
92+
confpy_version, confpy_release = get_setuppy_version(rootdir)
93+
changelog_version, changelog_release = get_setuppy_version(rootdir)
94+
95+
version_head = "Version"
96+
version_width = max(
97+
[
98+
len(version_head),
99+
len(setuppy_version),
100+
len(confpy_version),
101+
len(changelog_version),
102+
]
103+
)
104+
105+
release_head = "Release"
106+
release_width = max(
107+
[
108+
len(release_head),
109+
len(setuppy_release),
110+
len(confpy_release),
111+
len(changelog_release),
112+
]
113+
)
114+
115+
print(
116+
f"File {version_head} {release_head}\n"
117+
f"------------------------------- {'-' * version_width}"
118+
f" {'-' * release_width}\n"
119+
f"setup.py {setuppy_version:>{version_width}}"
120+
f" {setuppy_release:>{release_width}}\n"
121+
f"docs/conf.py {confpy_version:>{version_width}}"
122+
f" {confpy_release:>{release_width}}\n"
123+
f"docs/changelog.rst {changelog_version:>{version_width}}"
124+
f" {changelog_release:>{release_width}}\n"
125+
)
126+
127+
assert setuppy_version == confpy_version
128+
assert setuppy_version == changelog_version
129+
130+
assert setuppy_release == confpy_release
131+
assert setuppy_release == changelog_release
132+
133+
134+
if __name__ == "__main__":
135+
main()

0 commit comments

Comments
 (0)