Loading [MathJax]/extensions/tex2jax.js
The Gaudi Framework  v38r0 (2143aa4c)
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Modules Pages
update_version.py
Go to the documentation of this file.
1 #!/usr/bin/env python3
2 
12 import datetime
13 import re
14 import sys
15 from collections.abc import Callable, Iterable
16 from difflib import unified_diff
17 from subprocess import run
18 from typing import Union
19 
20 import click
21 
22 
23 def normalize_version(version: str) -> tuple[str, str]:
24  """
25  Convert a version in format "vXrY" or "X.Y" in the pair ("X.Y", "vXrY").
26 
27  >>> normalize_version("v37r0")
28  ('37.0', 'v37r0')
29  >>> normalize_version("37.0.1")
30  ('37.0.1', 'v37r0p1')
31  """
32  # extract the digits
33  numbers = re.findall(r"\d+", version)
34  return (
35  ".".join(numbers),
36  "".join("{}{}".format(*pair) for pair in zip("vrpt", numbers)),
37  )
38 
39 
40 class Fields:
41  """
42  Helper to carry the allowed fields for formatting replacement strings.
43 
44  >>> f = Fields("v37r1", datetime.date(2023, 9, 25))
45  >>> f
46  Fields('37.1', datetime.date(2023, 9, 25))
47  >>> f.data
48  {'cmake_version': '37.1', 'tag_version': 'v37r1', 'date': datetime.date(2023, 9, 25)}
49  """
50 
51  def __init__(self, version: str, date: datetime.date):
52  cmake_version, tag_version = normalize_version(version)
53  self._data = dict(
54  cmake_version=cmake_version,
55  tag_version=tag_version,
56  date=date,
57  )
58 
59  def __repr__(self):
60  return (
61  f"Fields({repr(self._data['cmake_version'])}, {repr(self._data['date'])})"
62  )
63 
64  @property
65  def data(self):
66  return self._data
67 
68 
70  """
71  Helper to replace lines with patterns or applying functions.
72 
73  >>> r = ReplacementRule(r"^version: ", "version: {cmake_version}")
74  >>> f = Fields("v1r1", datetime.date(2023, 9, 25))
75  >>> r("nothing to change\\n", f)
76  'nothing to change\\n'
77  >>> r("version: 1.0\\n", f)
78  'version: 1.1\\n'
79  """
80 
81  def __init__(
82  self,
83  pattern: Union[str, re.Pattern],
84  replace: Union[str, Callable[[str, Fields], str]],
85  ):
86  self.pattern = re.compile(pattern)
87  if isinstance(replace, str):
88  replace = f"{replace.rstrip()}\n"
89  self.replace = lambda _line, fields: replace.format(**fields.data)
90  else:
91  self.replace = replace
92 
93  def __call__(self, line: str, fields: Fields) -> str:
94  if self.pattern.match(line):
95  return self.replace(line, fields)
96  return line
97 
98 
100  def __init__(
101  self, filename: str, rules: Iterable[Union[ReplacementRule, tuple[str, str]]]
102  ):
103  self.filename = filename
104  self.rules = [
105  r if isinstance(r, ReplacementRule) else ReplacementRule(*r) for r in rules
106  ]
107 
108  def _apply_rules(self, line: str, fields: Fields) -> str:
109  for rule in self.rules:
110  line = rule(line, fields)
111  return line
112 
113  def __call__(self, fields: Fields) -> tuple[str, list[str], list[str]]:
114  with open(self.filename) as f:
115  old = f.readlines()
116  return self.filename, old, [self._apply_rules(line, fields) for line in old]
117 
118 
119 def update_changelog(fields: Fields) -> tuple[str, list[str], list[str]]:
120  """
121  Special updater to fill draft changelog entry.
122  """
123  latest_tag = run(
124  ["git", "describe", "--tags", "--abbrev=0"], capture_output=True, text=True
125  ).stdout.strip()
126  # This formats the git log as a rough markdown list
127  # - collect the log formatting it such that we can machine parse it
128  changes_txt = run(
129  ["git", "log", "--first-parent", "--format=%s<=>%b|", f"{latest_tag}.."],
130  capture_output=True,
131  text=True,
132  ).stdout
133  # - removing trailing separator and make it a single line
134  changes_txt = " ".join(changes_txt.strip().rstrip("|").splitlines())
135  # - normalize issues and merge requests links
136  changes = (
137  changes_txt.replace("Closes #", "gaudi/Gaudi#")
138  .replace("See merge request ", "")
139  .split("|")
140  )
141  # - split the messages and format the list
142  changes = [
143  f"- {msg.strip()} ({', '.join(refs.split())})\n"
144  if refs.strip()
145  else f"- {msg.strip()}\n"
146  for change in changes
147  for msg, refs in [change.split("<=>", 1)]
148  ]
149 
150  filename = "CHANGELOG.md"
151  with open(filename) as f:
152  old = f.readlines()
153  for idx, line in enumerate(old):
154  if line.startswith("## ["):
155  break
156 
157  data = old[:idx]
158  data.extend(
159  [
160  "## [{tag_version}](https://gitlab.cern.ch/gaudi/Gaudi/-/releases/{tag_version}) - {date}\n".format(
161  **fields.data
162  ),
163  "\n",
164  "### Changed\n",
165  "### Added\n",
166  "### Fixed\n",
167  "\n",
168  ]
169  )
170  data.extend(changes)
171  data.extend(["\n", "\n"])
172  data.extend(old[idx:])
173 
174  return filename, old, data
175 
176 
177 @click.command()
178 @click.argument("version", type=str)
179 @click.argument(
180  "date",
181  type=click.DateTime(("%Y-%m-%d",)),
182  metavar="[DATE]",
183  default=datetime.datetime.now(),
184 )
185 @click.option(
186  "--dry-run",
187  "-n",
188  default=False,
189  is_flag=True,
190  help="only show what would change, but do not modify the files",
191 )
192 def update_version(version: str, date: datetime.datetime, dry_run: bool):
193  """
194  Helper to easily update the project version number in all needed files.
195  """
196  fields = Fields(version, date.date())
197  click.echo(
198  "Bumping version to {cmake_version} (tag: {tag_version})".format(**fields.data)
199  )
200 
201  for updater in [
202  FileUpdater(
203  "CMakeLists.txt",
204  [(r"^project\‍(Gaudi VERSION", "project(Gaudi VERSION {cmake_version}")],
205  ),
206  FileUpdater(
207  "CITATION.cff",
208  [
209  (r"^version: ", "version: {tag_version}"),
210  (r"^date-released: ", "date-released: '{date}'"),
211  ],
212  ),
213  FileUpdater(
214  "docs/source/conf.py",
215  [
216  (r"^version = ", 'version = "{cmake_version}"'),
217  (r"^release = ", 'release = "{tag_version}"'),
218  ],
219  ),
220  update_changelog,
221  ]:
222  filename, old, new = updater(fields)
223 
224  if old != new:
225  if dry_run:
226  sys.stdout.writelines(
227  unified_diff(
228  old,
229  new,
230  fromfile=f"a/{filename}",
231  tofile=f"b/{filename}",
232  )
233  )
234  else:
235  click.echo(f"updated {filename}")
236  with open(filename, "w") as f:
237  f.writelines(new)
238 
239 
240 if __name__ == "__main__":
update_version.ReplacementRule.replace
replace
Definition: update_version.py:85
update_version.ReplacementRule
Definition: update_version.py:69
update_version.ReplacementRule.pattern
pattern
Definition: update_version.py:82
update_version.ReplacementRule.__init__
def __init__(self, Union[str, re.Pattern] pattern, Union[str, Callable[[str, Fields], str]] replace)
Definition: update_version.py:81
update_version.FileUpdater
Definition: update_version.py:99
update_version.FileUpdater.filename
filename
Definition: update_version.py:101
update_version
Definition: update_version.py:1
update_version.Fields
Definition: update_version.py:40
update_version.update_version
def update_version(str version, datetime.datetime date, bool dry_run)
Definition: update_version.py:192
update_version.FileUpdater._apply_rules
str _apply_rules(self, str line, Fields fields)
Definition: update_version.py:108
format
GAUDI_API std::string format(const char *,...)
MsgStream format utility "a la sprintf(...)".
Definition: MsgStream.cpp:119
update_version.FileUpdater.__call__
tuple[str, list[str], list[str]] __call__(self, Fields fields)
Definition: update_version.py:113
update_version.Fields._data
_data
Definition: update_version.py:53
update_version.Fields.data
def data(self)
Definition: update_version.py:65
update_version.FileUpdater.__init__
def __init__(self, str filename, Iterable[Union[ReplacementRule, tuple[str, str]]] rules)
Definition: update_version.py:100
update_version.Fields.__init__
def __init__(self, str version, datetime.date date)
Definition: update_version.py:51
update_version.ReplacementRule.__call__
str __call__(self, str line, Fields fields)
Definition: update_version.py:93
update_version.normalize_version
tuple[str, str] normalize_version(str version)
Definition: update_version.py:23
update_version.update_changelog
tuple[str, list[str], list[str]] update_changelog(Fields fields)
Definition: update_version.py:119
update_version.Fields.__repr__
def __repr__(self)
Definition: update_version.py:59
update_version.FileUpdater.rules
rules
Definition: update_version.py:102