multicz 0.3.0__tar.gz → 0.4.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: multicz
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Multi-component versioning for monorepos: bump apps, charts, and images independently from conventional commits.
5
5
  Keywords: semver,monorepo,helm,conventional-commits,release,versioning
6
6
  Author: Chris
@@ -960,6 +960,28 @@ exec multicz check "$1"
960
960
  * `.properties` — line-based `key=value` substitution (e.g. `gradle.properties`)
961
961
  * anything else — treated as a one-line `VERSION` file (`key = ` omitted)
962
962
 
963
+ ### Regex escape hatch (`key = "regex:..."`)
964
+
965
+ For files no structured parser handles — Python `__version__`,
966
+ TypeScript `export const VERSION`, Cargo `version = "..."` *outside*
967
+ the `[package]` table, Makefile `VERSION := ...`, shell scripts,
968
+ Dockerfile `LABEL version=...` — prefix the key with `regex:`:
969
+
970
+ ```toml
971
+ [components.api]
972
+ bump_files = [
973
+ { file = "pyproject.toml", key = "project.version" },
974
+ { file = "src/api/__init__.py", key = 'regex:^__version__\s*=\s*"([^"]+)"' },
975
+ ]
976
+ ```
977
+
978
+ The pattern needs exactly one capture group locating the version
979
+ literal. Matching uses `re.MULTILINE`, and only the *first* match's
980
+ capture group is rewritten — surrounding text (quotes, indentation,
981
+ comments, the rest of the file) is preserved byte-for-byte. Bad
982
+ patterns (uncompilable, no group, no match in file) surface at
983
+ `multicz validate --strict` rather than at bump time.
984
+
963
985
  ### Debian packages (`format = "debian"`)
964
986
 
965
987
  `multicz` writes a proper `debian/changelog` instead of a markdown
@@ -932,6 +932,28 @@ exec multicz check "$1"
932
932
  * `.properties` — line-based `key=value` substitution (e.g. `gradle.properties`)
933
933
  * anything else — treated as a one-line `VERSION` file (`key = ` omitted)
934
934
 
935
+ ### Regex escape hatch (`key = "regex:..."`)
936
+
937
+ For files no structured parser handles — Python `__version__`,
938
+ TypeScript `export const VERSION`, Cargo `version = "..."` *outside*
939
+ the `[package]` table, Makefile `VERSION := ...`, shell scripts,
940
+ Dockerfile `LABEL version=...` — prefix the key with `regex:`:
941
+
942
+ ```toml
943
+ [components.api]
944
+ bump_files = [
945
+ { file = "pyproject.toml", key = "project.version" },
946
+ { file = "src/api/__init__.py", key = 'regex:^__version__\s*=\s*"([^"]+)"' },
947
+ ]
948
+ ```
949
+
950
+ The pattern needs exactly one capture group locating the version
951
+ literal. Matching uses `re.MULTILINE`, and only the *first* match's
952
+ capture group is rewritten — surrounding text (quotes, indentation,
953
+ comments, the rest of the file) is preserved byte-for-byte. Bad
954
+ patterns (uncompilable, no group, no match in file) surface at
955
+ `multicz validate --strict` rather than at bump time.
956
+
935
957
  ### Debian packages (`format = "debian"`)
936
958
 
937
959
  `multicz` writes a proper `debian/changelog` instead of a markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "multicz"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  description = "Multi-component versioning for monorepos: bump apps, charts, and images independently from conventional commits."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -13,22 +13,54 @@ A ``key`` is a dotted path (``project.version``, ``image.tag``). Passing
13
13
  files the dotted-path interpretation is disabled — the key is taken
14
14
  verbatim, since properties files routinely use dotted keys (``a.b.c``)
15
15
  that are *not* nested.
16
+
17
+ A ``key`` prefixed with ``regex:`` is a language-agnostic escape hatch:
18
+ the rest of the string is a regex with one capture group locating the
19
+ version literal. Useful for ``__version__ = "X"`` in Python,
20
+ ``export const VERSION = "X"`` in TypeScript, ``VERSION := X`` in
21
+ Makefiles, etc. The regex is anchored with :data:`re.MULTILINE`, and
22
+ only the *first* match's capture group is rewritten.
16
23
  """
17
24
 
18
25
  from __future__ import annotations
19
26
 
20
27
  import io
21
28
  import json
29
+ import re
22
30
  from pathlib import Path
23
31
 
24
32
  import tomlkit
25
33
  from ruamel.yaml import YAML
26
34
 
35
+ REGEX_KEY_PREFIX = "regex:"
36
+
27
37
 
28
38
  class WriterError(RuntimeError):
29
39
  """Raised when a value cannot be read or written."""
30
40
 
31
41
 
42
+ def _is_regex_key(key: str | None) -> bool:
43
+ return key is not None and key.startswith(REGEX_KEY_PREFIX)
44
+
45
+
46
+ def _compile_regex_key(key: str) -> re.Pattern[str]:
47
+ pattern = key[len(REGEX_KEY_PREFIX):]
48
+ if not pattern:
49
+ raise WriterError(f"empty regex pattern in key: {key!r}")
50
+ try:
51
+ compiled = re.compile(pattern, re.MULTILINE)
52
+ except re.error as exc:
53
+ raise WriterError(
54
+ f"invalid regex {pattern!r} in key {key!r}: {exc}"
55
+ ) from exc
56
+ if compiled.groups < 1:
57
+ raise WriterError(
58
+ f"regex {pattern!r} must contain exactly one capture "
59
+ "group locating the version literal"
60
+ )
61
+ return compiled
62
+
63
+
32
64
  def _split_key(key: str) -> list[str]:
33
65
  parts = [part for part in key.split(".") if part]
34
66
  if not parts:
@@ -121,6 +153,14 @@ def read_value(file: Path, key: str | None) -> str:
121
153
  text = file.read_text(encoding="utf-8")
122
154
  if key is None:
123
155
  return text.strip()
156
+ if _is_regex_key(key):
157
+ pattern = _compile_regex_key(key)
158
+ match = pattern.search(text)
159
+ if not match:
160
+ raise WriterError(
161
+ f"regex {key!r} matched nothing in {file}"
162
+ )
163
+ return match.group(1)
124
164
  if _is_properties(file):
125
165
  result = _read_property(text, key)
126
166
  if result is None:
@@ -157,6 +197,22 @@ def write_value(file: Path, key: str | None, value: str) -> None:
157
197
  file.write_text(value + "\n", encoding="utf-8")
158
198
  return
159
199
 
200
+ if _is_regex_key(key):
201
+ pattern = _compile_regex_key(key)
202
+ text = file.read_text(encoding="utf-8")
203
+ match = pattern.search(text)
204
+ if not match:
205
+ raise WriterError(
206
+ f"regex {key!r} matched nothing in {file}"
207
+ )
208
+ # Replace only the first match's capture group, preserving the
209
+ # surrounding text byte-for-byte (quotes, indentation, comments).
210
+ g_start, g_end = match.span(1)
211
+ file.write_text(
212
+ text[:g_start] + value + text[g_end:], encoding="utf-8"
213
+ )
214
+ return
215
+
160
216
  if _is_properties(file):
161
217
  text = file.read_text(encoding="utf-8")
162
218
  file.write_text(_write_property(text, key, value), encoding="utf-8")
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes