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