1+ from pathlib import Path
2+ import re
3+ import sys
4+ import tempfile
5+
6+
7+ SECTION_RE = re .compile (r"^\s*\[(?P<name>[^\[\]]+)\]\s*(?:#.*)?$" )
8+ VERSION_RE = re .compile (r'^(?P<prefix>[ \t]*version\s*=\s*")(?P<version>[^"]+)(?P<suffix>".*)$' )
9+
10+
11+ def find_section_bounds (lines : list [str ], section_name : str , target_file : Path ) -> tuple [int , int ]:
12+ in_target_section = False
13+ section_start = None
14+
15+ for index , line in enumerate (lines ):
16+ match = SECTION_RE .match (line )
17+ if not match :
18+ continue
19+
20+ current_section = match .group ("name" ).strip ()
21+ if in_target_section :
22+ return section_start , index
23+
24+ if current_section == section_name :
25+ in_target_section = True
26+ section_start = index + 1
27+
28+ if in_target_section :
29+ return section_start , len (lines )
30+
31+ raise RuntimeError (f"Section [{ section_name } ] not found in { target_file } " )
32+
33+
34+ def append_suffix_to_version (lines : list [str ], start : int , end : int , suffix : str , target_file : Path ) -> bool :
35+ for index in range (start , end ):
36+ match = VERSION_RE .match (lines [index ])
37+ if not match :
38+ continue
39+
40+ current_version = match .group ("version" )
41+ if current_version .endswith (suffix ):
42+ return False
43+
44+ lines [index ] = (
45+ f"{ match .group ('prefix' )} { current_version } { suffix } { match .group ('suffix' )} "
46+ )
47+ return True
48+
49+ raise RuntimeError (f"version entry not found in [package] section of { target_file } " )
50+
51+
52+ def update_manifest (target_file : Path , suffix : str ) -> bool :
53+ with target_file .open ("r" , encoding = "utf-8" , newline = "" ) as file :
54+ lines = file .readlines ()
55+
56+ package_start , package_end = find_section_bounds (lines , "package" , target_file )
57+ changed = append_suffix_to_version (lines , package_start , package_end , suffix , target_file )
58+
59+ if changed :
60+ with target_file .open ("w" , encoding = "utf-8" , newline = "" ) as file :
61+ file .writelines (lines )
62+
63+ return changed
64+
65+
66+ def assert_equal (actual : object , expected : object , message : str ) -> None :
67+ if actual != expected :
68+ raise AssertionError (f"{ message } : expected { expected !r} , got { actual !r} " )
69+
70+
71+ def assert_raises (function , expected_message : str ) -> None :
72+ try :
73+ function ()
74+ except RuntimeError as error :
75+ if expected_message not in str (error ):
76+ raise AssertionError (
77+ f"unexpected error message: expected to contain { expected_message !r} , got { str (error )!r} "
78+ ) from error
79+ return
80+
81+ raise AssertionError ("expected RuntimeError was not raised" )
82+
83+
84+ def run_self_test () -> int :
85+ suffix = "-dev1234"
86+ valid_manifest = """[package]
87+ name = "example"
88+ version = "0.1.0"
89+
90+ [dependencies]
91+ serde = "1"
92+ """
93+
94+ with tempfile .TemporaryDirectory () as temp_dir :
95+ manifest_path = Path (temp_dir ) / "Cargo.toml"
96+
97+ manifest_path .write_text (
98+ valid_manifest ,
99+ encoding = "utf-8" ,
100+ newline = "" ,
101+ )
102+ changed = update_manifest (manifest_path , suffix )
103+ updated_manifest = manifest_path .read_text (encoding = "utf-8" )
104+
105+ assert_equal (changed , True , "first update should modify the manifest" )
106+ assert_equal (
107+ 'version = "0.1.0-dev1234"' in updated_manifest ,
108+ True ,
109+ "version suffix should be appended in [package]" ,
110+ )
111+ assert_equal (
112+ 'serde = "1"' in updated_manifest ,
113+ True ,
114+ "entries outside [package] should remain untouched" ,
115+ )
116+
117+ changed = update_manifest (manifest_path , suffix )
118+ assert_equal (changed , False , "second update should be idempotent" )
119+
120+ missing_section_path = Path (temp_dir ) / "missing-section.toml"
121+ missing_section_path .write_text (
122+ """[dependencies]\n serde = \" 1\" \n """ ,
123+ encoding = "utf-8" ,
124+ newline = "" ,
125+ )
126+ assert_raises (
127+ lambda : update_manifest (missing_section_path , suffix ),
128+ "Section [package] not found" ,
129+ )
130+
131+ missing_version_path = Path (temp_dir ) / "missing-version.toml"
132+ missing_version_path .write_text (
133+ """[package]\n name = \" example\" \n """ ,
134+ encoding = "utf-8" ,
135+ newline = "" ,
136+ )
137+ assert_raises (
138+ lambda : update_manifest (missing_version_path , suffix ),
139+ "version entry not found" ,
140+ )
141+
142+ print ("self-test passed" )
143+ return 0
144+
145+
146+ def main (argv : list [str ] | None = None ) -> int :
147+ args = list (sys .argv [1 :] if argv is None else argv )
148+
149+ if args == ["--self-test" ]:
150+ return run_self_test ()
151+
152+ if len (args ) != 2 :
153+ raise SystemExit ("usage: set_dev_version.py <manifest_path> <suffix> | --self-test" )
154+
155+ update_manifest (Path (args [0 ]), args [1 ])
156+ return 0
157+
158+
159+ if __name__ == "__main__" :
160+ raise SystemExit (main ())
0 commit comments