-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathvbump.py
More file actions
407 lines (323 loc) · 15 KB
/
vbump.py
File metadata and controls
407 lines (323 loc) · 15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
import sys
import re
import argparse
import textwrap
import config
from util import starprint
import util
import _version
# command line arguments stored here
args = argparse.Namespace()
#
#
def increment(value: str) -> str:
"""
Perform increment operation on string representation of a value, such as what would be obtained from an ini file
Args:
value: value to be incremented, e.g. '3' becomes '4'
Returns:
string representation of incremented value
"""
rv = None
if value.isdecimal():
rv = f'{int(value) + 1}'
return rv
#
#
def bump(fieldname: str = None) -> dict:
"""
Bump operation, to
- 'auto' fields are incremented by 1
- increment indicated fieldname by 1, and reset all lower fields (as defined in reset_order) to 0
Args:
fieldname: fieldname to be incremented, or 'None' to only increment 'auto' fields
Returns:
updated dictionary of {key:val} where keys = fieldnames, val = field value
"""
reset_order = config.config_data['bump']['reset_order']
reset_list = reset_order.split(', ')
# get current version info from config file
current_version_dict = config.config_data['current_version']
# make a copy for modification
new_version_dict = {}
for key in current_version_dict.keys():
new_version_dict[key] = current_version_dict[key]
# to help with the reset logic later, create a dictionary of (k:v) of (fieldnames:reset_order)
# the logic being, if we are going to bump field at index N, then all fields at index >N will reset to 0
reset_dict = {}
i = 0
for field in reset_list:
reset_dict[field] = i
i += 1
# start by incrementing all fields in bump.auto list
auto_list = config.config_data['bump']['auto'].split(', ')
for field in auto_list:
if field in new_version_dict:
cur_value = new_version_dict[field]
new_value = increment(cur_value)
if new_value:
new_version_dict[field] = new_value
# then bumping the requested field
# note that if the requested field has already been bumped in the auto_list, don't bump it again
if fieldname in new_version_dict and fieldname not in auto_list:
cur_value = new_version_dict[fieldname]
new_value = increment(cur_value)
if new_value:
new_version_dict[fieldname] = new_value
# if we successfully bumped the requested field, now reset all downstream fields, as defined in
# the [bump][reset_order] value of the ini file
if fieldname in reset_dict:
for key in reset_dict.keys():
# use the reset_dict dictionary we created earlier to determine if each field should be reset
if reset_dict[key] > reset_dict[fieldname]:
new_version_dict[key] = '0'
return new_version_dict
#
#
def version(write_pattern: str, version_dict: dict) -> str:
"""
create version string, using the f-string pattern in write_pattern, and field values are from the version_dict
TODO note that a field present in the write_pattern, but not in the list of fields in version_dict,
will cause an exception which we don't catch, because there is no graceful recovery
Args:
write_pattern: f-string format for the created string
version_dict: dictionary of {key:val} where keys = fieldnames, val = field value
Returns:
string containing current version, in write_pattern format
"""
# use **kwargs formatting to translate fieldnames into the write_pattern
rv = write_pattern.format(**version_dict)
return rv
#
#
def write():
if not args.quiet:
if args.write:
output_format = args.write
else:
output_format = 'dev'
starprint(f'Writing output files, format [{output_format}]', fill='=', alignment='^')
# get the list of output filenames from the ini file
write_files = config.config_data['write']['files']
write_file_list = write_files.split(', ')
# walk the list of files
files_modified = 0
for filename in write_file_list:
if not args.quiet:
starprint(f'Writing output file: [{filename}]', fill='-', alignment='^')
try:
# read entire contents of file into a list
with open(filename, 'r') as f:
line_list = f.readlines()
# walk the list of lines, looking for version strings
lines_modified = 0
for ndx, line in enumerate(line_list):
# parse each line for the presence of a version string
newline = parse(line)
if newline and newline != line:
# replace the original line in the list with the new one
line_list.pop(ndx)
line_list.insert(ndx, newline)
# print summary
if not args.quiet:
# starprint(f'Current version line: {line}', end='')
# starprint(f'New version line : {newline}', end='')
starprint(f'Current version line: {line}')
starprint(f'New version line : {newline}')
# increment the lines modified counter
lines_modified += 1
# show how many lines were modified
if not args.quiet:
starprint(f'Lines to be modified: {lines_modified}')
# if not dryrun, write out results
# todo save prev files as .bak versions??
if not args.dry_run:
with open(filename, 'w') as f:
f.writelines(line_list)
starprint(f'File saved : {filename}')
files_modified += 1
except FileNotFoundError as fnf:
if not args.quiet:
print(fnf)
starprint(f'Output files modified: {files_modified}', fill='-', alignment='^')
#
#
def parse(line: str) -> None or str:
"""
Parse a given line of text for the presence of a version string, as defined by
'read_regex' in the configuration ini file
Args:
line: line of text to be parsed
Returns:
None if no version string is found, or modified line with new version string
"""
# set up the read pattern
regex = config.config_data['syntax']['read_regex']
pre_regex = '(?P<pre>.*)'
post_regex = '(?P<post>.*)'
full_regex = pre_regex + regex + post_regex
# set up the write pattern
if args.write == 'dev':
key = 'write_dev'
elif args.write == 'prod':
key = 'write_prod'
else:
key = 'write_dev'
write_pattern = config.config_data['syntax'][key]
pre_pattern = '{pre}'
post_pattern = '{post}'
full_pattern = pre_pattern + write_pattern + post_pattern
# default return value
rv = None
# check this line for presence of a version string
m = re.match(full_regex, line)
if m:
pre_value = m.group('pre')
post_value = m.group('post')
# get current version dictionary, and add the pre and post values to it
version_dict = config.config_data['current_version']
version_dict['pre'] = pre_value
version_dict['post'] = post_value
# use the **kwargs format to smerge together the f-string write pattern with the dictionary of field values
newline = full_pattern.format(**version_dict)
# update the return value
rv = newline + '\n'
# return value
return rv
#
#
def main():
# *********************************************************************************************************
# parse the command line
def formatter(prog): return argparse.RawTextHelpFormatter(prog, max_help_position=52)
cli_parser = argparse.ArgumentParser(
# formatter_class=argparse.RawTextHelpFormatter,
formatter_class=formatter,
# formatter_class=argparse.RawDescriptionHelpFormatter,
description=textwrap.dedent(f'''
Command line tool to automate version bumping.
- No command line options is equivalent to '--bump' and '--write'
- Master version maintained in [{config.ini_filename}]
'''))
# report current version
cli_parser.add_argument('-c', '--current-version',
help="Return current version string in 'dev' (default) or 'prod' format\n"
f"Reads version info from [current_version] section of [{config.ini_filename}]\n"
f"String formatted as indicated in [syntax] section of [{config.ini_filename}]\n",
nargs='?', type=str, const='dev', choices=['dev', 'prod'])
# bump commands
cli_parser.add_argument('-b', '--bump',
help=f"Bump the indicated field\n"
f"Default = 'auto' fields in [bump] section of [{config.ini_filename}]\n"
f"Reads from, and writes to [current_version] section of [{config.ini_filename}]",
nargs='?', type=str, const='auto')
# write current version to output files
cli_parser.add_argument('-w', '--write',
help=f"Writes version string in 'dev' (default) or 'prod' format into the output file(s)\n"
f"Reads version info from [current_version] section of [{config.ini_filename}]\n"
f"Writes to output files as specified in the [write] section of [{config.ini_filename}]",
nargs='?', type=str, const='dev', choices=['dev', 'prod'])
# dry run?
cli_parser.add_argument('-d', '--dry-run',
help='flag: Report what actions will be taken, but do not actually take them',
action='store_true')
# quiet
cli_parser.add_argument('-q', '--quiet',
help='flag: Perform all actions with no screen reports',
action='store_true')
# init
cli_parser.add_argument('-i', '--init',
help='flag: Print sample config files to screen (stdout)',
action='store_true')
# version of vbump
cli_parser.add_argument('-v', '--version',
help='flag: Print version of vbump and exit',
action='store_true')
# parse the command line
global args
args = cli_parser.parse_args()
# todo - add a verbose option and include this in output? very useful for debugging
# if not args.quiet:
# print(args)
# *********************************************************************************************************
# load the ini file
configloaded = config.load()
# *********************************************************************************************************
# process init command and exit
# do this before the check on configloaded, so even if the config file wasn't loaded, the user still
# has the chance to issue the --init command to create the ini file
if args.init:
if not args.quiet:
util.print_example_files()
sys.exit(0)
# process vbump version command
if args.version:
if not args.quiet:
print(f'{_version.__VERSION__}')
sys.exit(0)
# was there a problem loading the ini file? if so, bail out
if not configloaded:
sys.exit(0)
# make a copy of the version info dictionary
new_version_dict = {}
current_version_dict = config.config_data['current_version']
for key in current_version_dict.keys():
new_version_dict[key] = current_version_dict[key]
# no command given on command line? interpret this as --bump and --write
bumpwrite = False
if args.current_version is None and args.bump is None and args.write is None:
bumpwrite = True
# process version command
if args.current_version:
# it is a bit amazing that this works. Handy that format() is written to properly deal with **kwargs, which as it happens,
# the dictionary representation of the config.config objects are in exactly the right format to support
write_dev = config.config_data['syntax']['write_dev']
write_prod = config.config_data['syntax']['write_prod']
if args.current_version == 'dev':
if not args.quiet:
# print(f'Current version (dev format): {version(write_dev, current_version_dict)}')
print(f'{version(write_dev, current_version_dict)}')
elif args.current_version == 'prod':
if not args.quiet:
# print(f'Current version (prod format): {version(write_prod, current_version_dict)}')
print(f'{version(write_prod, current_version_dict)}')
# process bump command
if args.bump or bumpwrite:
if not args.quiet:
starprint(f'Bumping version info from [{config.ini_filename}]', fill='=', alignment='^')
# current_version_dict = config.config_data['current_version']
write_dev = config.config_data['syntax']['write_dev']
# determine which fieldname to bump
if args.bump and args.bump in current_version_dict.keys():
fieldname = args.bump
else:
fieldname = None
if args.bump and args.bump != 'auto' and not args.quiet:
print(f" ['{args.bump}'] unrecognized field name")
print(f' Valid field names: {list(current_version_dict.keys())}')
# do the bump and report what new version will be
new_version_dict = bump(fieldname)
if not args.quiet:
starprint(f'Current version (dev format): {version(write_dev, current_version_dict)}')
starprint(f'New version (dev format): {version(write_dev, new_version_dict)}')
# if any fields have changed, then save them back to the current dictionary, and write it to disk
modified = False
if args.dry_run is False:
for fieldname in current_version_dict.keys():
new_val = new_version_dict[fieldname]
current_val = current_version_dict[fieldname]
if new_val != current_val:
config.config_data['current_version'][fieldname] = new_val
modified = True
if modified:
config.save()
if not args.quiet:
starprint(f'Updated version info saved to ini file [{config.ini_filename}]')
else:
if not args.quiet:
starprint(f'File [{config.ini_filename}] unchanged')
# process write command
if args.write or bumpwrite:
write()
if __name__ == '__main__':
main()