The Gaudi Framework
master (6507eef1)
Toggle main menu visibility
Loading...
Searching...
No Matches
update_version.py
Go to the documentation of this file.
1
#!/usr/bin/env python3
2
12
import
datetime
13
import
functools
14
import
os
15
import
re
16
import
sys
17
from
collections.abc
import
Callable, Iterable
18
from
difflib
import
unified_diff
19
from
subprocess
import
run
20
21
import
click
22
23
24
@functools.cache
25
def
gitlab_token
() -> str | None:
26
"""
27
Return the GitLab API token to use, if available.
28
29
The token is taken from the ``GITLAB_TOKEN`` environment variable or, as a
30
fallback, from the file indicated by ``GITLAB_TOKEN_FILE`` (default
31
``~/.gitlab_token``). The file, if present, must contain only the token.
32
"""
33
if
token := os.environ.get(
"GITLAB_TOKEN"
):
34
return
token
35
token_file = os.environ.get(
"GITLAB_TOKEN_FILE"
,
"~/.gitlab_token"
)
36
try
:
37
with
open(os.path.expanduser(token_file))
as
f:
38
return
f.read().strip()
or
None
39
except
OSError:
40
return
None
41
42
43
def
normalize_version
(version: str) -> tuple[str, str]:
44
"""
45
Convert a version in format "vXrY" or "X.Y" in the pair ("X.Y", "vXrY").
46
47
>>> normalize_version("v37r0")
48
('37.0', 'v37r0')
49
>>> normalize_version("37.0.1")
50
('37.0.1', 'v37r0p1')
51
"""
52
# extract the digits
53
numbers = re.findall(
r"\d+"
, version)
54
return
(
55
"."
.join(numbers),
56
""
.join(
"{}{}"
.
format
(*pair)
for
pair
in
zip(
"vrpt"
, numbers)),
57
)
58
59
60
class
Fields
:
61
"""
62
Helper to carry the allowed fields for formatting replacement strings.
63
64
>>> f = Fields("v37r1", datetime.date(2023, 9, 25))
65
>>> f
66
Fields('37.1', datetime.date(2023, 9, 25))
67
>>> f.data
68
{'cmake_version': '37.1', 'tag_version': 'v37r1', 'date': datetime.date(2023, 9, 25)}
69
"""
70
71
def
__init__
(self, version: str, date: datetime.date):
72
cmake_version, tag_version =
normalize_version
(version)
73
self.
_data
= dict(
74
cmake_version=cmake_version,
75
tag_version=tag_version,
76
date=date,
77
)
78
79
def
__repr__
(self):
80
return
(
81
f
"Fields({repr(self._data['cmake_version'])}, {repr(self._data['date'])})"
82
)
83
84
@property
85
def
data
(self):
86
return
self.
_data
87
88
89
class
ReplacementRule
:
90
"""
91
Helper to replace lines with patterns or applying functions.
92
93
>>> r = ReplacementRule(r"^version: ", "version: {cmake_version}")
94
>>> f = Fields("v1r1", datetime.date(2023, 9, 25))
95
>>> r("nothing to change\\n", f)
96
'nothing to change\\n'
97
>>> r("version: 1.0\\n", f)
98
'version: 1.1\\n'
99
"""
100
101
def
__init__
(
102
self,
103
pattern: str | re.Pattern,
104
replace: str | Callable[[str, Fields], str],
105
):
106
self.
pattern
= re.compile(pattern)
107
if
isinstance(replace, str):
108
replace = f
"{replace.rstrip()}\n"
109
self.
replace
=
lambda
_line, fields: replace.format(**fields.data)
110
else
:
111
self.
replace
= replace
112
113
def
__call__
(self, line: str, fields: Fields) -> str:
114
if
self.
pattern
.match(line):
115
return
self.
replace
(line, fields)
116
return
line
117
118
119
class
FileUpdater
:
120
def
__init__
(
121
self, filename: str, rules: Iterable[ReplacementRule | tuple[str, str]]
122
):
123
self.
filename
= filename
124
self.
rules
= [
125
r
if
isinstance(r, ReplacementRule)
else
ReplacementRule
(*r)
for
r
in
rules
126
]
127
128
def
_apply_rules
(self, line: str, fields: Fields) -> str:
129
for
rule
in
self.
rules
:
130
line = rule(line, fields)
131
return
line
132
133
def
__call__
(self, fields: Fields) -> tuple[str, list[str], list[str]]:
134
with
open(self.
filename
)
as
f:
135
old = f.readlines()
136
return
self.
filename
, old, [self.
_apply_rules
(line, fields)
for
line
in
old]
137
138
139
def
update_changelog
(fields: Fields) -> tuple[str, list[str], list[str]]:
140
"""
141
Special updater to fill draft changelog entry.
142
"""
143
latest_tag = run(
144
[
"git"
,
"describe"
,
"--tags"
,
"--abbrev=0"
], capture_output=
True
, text=
True
145
).stdout.strip()
146
# This formats the git log as a rough markdown list
147
# - collect the log formatting it such that we can machine parse it
148
changes_txt = run(
149
[
"git"
,
"log"
,
"--first-parent"
,
"--format=%s<=>%b|"
, f
"{latest_tag}.."
],
150
capture_output=
True
,
151
text=
True
,
152
).stdout
153
# - removing trailing separator and make it a single line
154
changes_txt =
" "
.join(changes_txt.strip().rstrip(
"|"
).splitlines())
155
# - normalize issues and merge requests links
156
changes = (
157
changes_txt.replace(
"Closes #"
,
"gaudi/Gaudi#"
)
158
.replace(
"See merge request "
,
""
)
159
.split(
"|"
)
160
)
161
# - split the messages and format the list
162
changes = [
163
f
"- {msg.strip()} ({', '.join(refs.split())})\n"
164
if
refs.strip()
165
else
f
"- {msg.strip()}\n"
166
for
change
in
changes
167
for
msg, refs
in
([change.split(
"<=>"
, 1)]
if
"<=>"
in
change
else
[])
168
]
169
# Now we get the list of contributors
170
contributors = sorted(
171
contributor_handle
(name)
172
for
name
in
set(
173
run(
174
[
"git"
,
"log"
,
"--format=%an"
, f
"{latest_tag}...HEAD"
],
175
capture_output=
True
,
176
text=
True
,
177
).stdout.splitlines()
178
)
179
)
180
181
filename =
"CHANGELOG.md"
182
with
open(filename)
as
f:
183
old = f.readlines()
184
for
idx, line
in
enumerate(old):
185
if
line.startswith(
"## ["
):
186
break
187
188
data = old[:idx]
189
data.extend(
190
[
191
"## [{tag_version}](https://gitlab.cern.ch/gaudi/Gaudi/-/releases/{tag_version}) - {date}\n"
.
format
(
192
**fields.data
193
),
194
"\nA special thanks to all the people that contributed to this release:\n"
,
195
",\n"
.join(contributors),
196
".\n\n"
,
197
"### Changed\n"
,
198
"### Added\n"
,
199
"### Fixed\n"
,
200
"\n"
,
201
]
202
)
203
data.extend(changes)
204
data.extend([
"\n"
,
"\n"
])
205
data.extend(old[idx:])
206
207
return
filename, old, data
208
209
210
def
contributor_handle
(name: str) -> str:
211
if
token :=
gitlab_token
():
212
from
requests
import
get
213
214
users = get(
215
"https://gitlab.cern.ch/api/v4/users"
,
216
headers={
"PRIVATE-TOKEN"
: token},
217
params={
"search"
: name},
218
).json()
219
if
users:
220
return
f
'@{users[0]["username"]}'
221
return
name
222
223
224
@click.command()
225
@click.argument("version", type=str)
226
@click.argument
(
227
"date"
,
228
type=click.DateTime((
"%Y-%m-%d"
,)),
229
metavar=
"[DATE]"
,
230
default=datetime.datetime.now(),
231
)
232
@click.option
(
233
"--dry-run"
,
234
"-n"
,
235
default=
False
,
236
is_flag=
True
,
237
help=
"only show what would change, but do not modify the files"
,
238
)
239
def
update_version
(version: str, date: datetime.datetime, dry_run: bool):
240
"""
241
Helper to easily update the project version number in all needed files.
242
"""
243
fields =
Fields
(version, date.date())
244
click.echo(
245
"Bumping version to {cmake_version} (tag: {tag_version})"
.
format
(**fields.data)
246
)
247
248
for
updater
in
[
249
FileUpdater
(
250
"CMakeLists.txt"
,
251
[(
r"^project\(Gaudi VERSION"
,
"project(Gaudi VERSION {cmake_version}"
)],
252
),
253
FileUpdater
(
254
"CITATION.cff"
,
255
[
256
(
r"^version: "
,
"version: {tag_version}"
),
257
(
r"^date-released: "
,
"date-released: '{date}'"
),
258
],
259
),
260
FileUpdater
(
261
"docs/source/conf.py"
,
262
[
263
(
r"^version = "
,
'version = "{cmake_version}"'
),
264
(
r"^release = "
,
'release = "{tag_version}"'
),
265
],
266
),
267
FileUpdater
(
268
"pixi.toml"
,
269
[
270
# Only project version values, i.e. those that look like a
271
# version, so that we do not touch e.g. the `"*"` required by
272
# the pixi-build backend.
273
(
r'^version = "\d'
,
'version = "{cmake_version}"'
),
274
],
275
),
276
update_changelog,
277
]:
278
filename, old, new = updater(fields)
279
280
if
old != new:
281
if
dry_run:
282
sys.stdout.writelines(
283
unified_diff(
284
old,
285
new,
286
fromfile=f
"a/{filename}"
,
287
tofile=f
"b/{filename}"
,
288
)
289
)
290
else
:
291
click.echo(f
"updated {filename}"
)
292
with
open(filename,
"w"
)
as
f:
293
f.writelines(new)
294
295
296
if
__name__ ==
"__main__"
:
297
update_version
()
format
GAUDI_API std::string format(const char *,...)
MsgStream format utility "a la sprintf(...)".
Definition
MsgStream.cpp:93
update_version.Fields
Definition
update_version.py:60
update_version.Fields.data
data(self)
Definition
update_version.py:85
update_version.Fields._data
_data
Definition
update_version.py:73
update_version.Fields.__repr__
__repr__(self)
Definition
update_version.py:79
update_version.Fields.__init__
__init__(self, str version, datetime.date date)
Definition
update_version.py:71
update_version.FileUpdater
Definition
update_version.py:119
update_version.FileUpdater.__init__
__init__(self, str filename, Iterable[ReplacementRule|tuple[str, str]] rules)
Definition
update_version.py:122
update_version.FileUpdater.rules
list rules
Definition
update_version.py:124
update_version.FileUpdater._apply_rules
str _apply_rules(self, str line, Fields fields)
Definition
update_version.py:128
update_version.FileUpdater.__call__
tuple[str, list[str], list[str]] __call__(self, Fields fields)
Definition
update_version.py:133
update_version.FileUpdater.filename
filename
Definition
update_version.py:123
update_version.ReplacementRule
Definition
update_version.py:89
update_version.ReplacementRule.pattern
pattern
Definition
update_version.py:106
update_version.ReplacementRule.__call__
str __call__(self, str line, Fields fields)
Definition
update_version.py:113
update_version.ReplacementRule.__init__
__init__(self, str|re.Pattern pattern, str|Callable[[str, Fields], str] replace)
Definition
update_version.py:105
update_version.ReplacementRule.replace
replace
Definition
update_version.py:109
update_version
Definition
update_version.py:1
update_version.contributor_handle
str contributor_handle(str name)
Definition
update_version.py:210
update_version.update_changelog
tuple[str, list[str], list[str]] update_changelog(Fields fields)
Definition
update_version.py:139
update_version.normalize_version
tuple[str, str] normalize_version(str version)
Definition
update_version.py:43
update_version.gitlab_token
str|None gitlab_token()
Definition
update_version.py:25
utils
update_version.py
Generated on
for The Gaudi Framework by
1.17.0