Coverage for src/cli/main.py: 95.87%

121 statements  

« prev     ^ index     » next       coverage.py v7.17.2, created at 2026-11-08 13:49 +0000

1#!/usr/bin/env python3 

2 

3# Standard libraries 

4from argparse import ( 

5 _ArgumentGroup, 

6 _MutuallyExclusiveGroup, 

7 ArgumentParser, 

8 Namespace, 

9 RawTextHelpFormatter, 

10) 

11from os import environ 

12from re import fullmatch 

13from shutil import get_terminal_size 

14from sys import exit as sys_exit 

15 

16# Components 

17from ..features.precommit import PreCommit 

18from ..features.prek import Prek 

19from ..package.bundle import Bundle 

20from ..package.settings import Settings 

21from ..package.updates import Updates 

22from ..package.version import Version 

23from ..prints.colors import Colors 

24from ..system.platform import Platform 

25from ..types.environments import Environments 

26from .entrypoint import Entrypoint 

27 

28# Constants 

29HELP_POSITION: int = 25 

30 

31# Main, pylint: disable=too-many-branches,too-many-statements 

32def main() -> None: 

33 

34 # Variables 

35 environments: Environments 

36 group: _ArgumentGroup 

37 result: Entrypoint.Result = Entrypoint.Result.ERROR 

38 subgroup: _MutuallyExclusiveGroup 

39 

40 # Configure environment variables 

41 environments = Environments() 

42 

43 # Configure pre-commit 

44 PreCommit.configure() 

45 

46 # Arguments creation 

47 parser: ArgumentParser = ArgumentParser( 

48 prog=Bundle.NAME, 

49 description=f'{Bundle.NAME}: {Bundle.DESCRIPTION}', 

50 epilog=environments.help(HELP_POSITION), 

51 add_help=False, 

52 formatter_class=lambda prog: RawTextHelpFormatter( 

53 prog, 

54 max_help_position=HELP_POSITION, 

55 width=min( 

56 120, 

57 get_terminal_size().columns - 2, 

58 ), 

59 ), 

60 ) 

61 

62 # Arguments internal definitions 

63 group = parser.add_argument_group('internal arguments') 

64 group.add_argument( 

65 '-h', 

66 '--help', 

67 dest='help', 

68 action='store_true', 

69 help='Show this help message', 

70 ) 

71 group.add_argument( 

72 '--version', 

73 dest='version', 

74 action='store_true', 

75 help='Show the current version', 

76 ) 

77 group.add_argument( 

78 '--no-color', 

79 dest='no_color', 

80 action='store_true', 

81 help=f'Disable colors outputs with \'{Bundle.ENV_NO_COLOR}=1\'\n' 

82 '(or default settings: [themes] > no_color)', 

83 ) 

84 group.add_argument( 

85 '--update-check', 

86 dest='update_check', 

87 action='store_true', 

88 help='Check for newer package updates', 

89 ) 

90 group.add_argument( 

91 '--settings', 

92 dest='settings', 

93 action='store_true', 

94 help='Show the current settings path and contents', 

95 ) 

96 group.add_argument( 

97 '--set', 

98 dest='set', 

99 action='store', 

100 metavar=('GROUP', 'KEY', 'VAL'), 

101 nargs=3, 

102 help='Set settings specific \'VAL\' value to [GROUP] > KEY\n' \ 

103 'or unset by using \'UNSET\' as \'VAL\'', 

104 ) 

105 

106 # Arguments modes definitions 

107 group = parser.add_argument_group('modes arguments') 

108 subgroup = group.add_mutually_exclusive_group() 

109 subgroup.add_argument( 

110 '-l', 

111 '--list', 

112 dest='list', 

113 action='store_true', 

114 help='List Git hooks installed in sources', 

115 ) 

116 subgroup.add_argument( 

117 '-i', 

118 '--install', 

119 dest='install', 

120 action='store_true', 

121 help='Install dependencies for pre-commit hooks', 

122 ) 

123 subgroup.add_argument( 

124 '-c', 

125 '--configure', 

126 dest='configure', 

127 action='store_true', 

128 help='Update sources with hooks configurations', 

129 ) 

130 subgroup.add_argument( 

131 '-b', 

132 '--badges', 

133 dest='badges', 

134 action='store_true', 

135 help='Update documentation with badges configurations', 

136 ) 

137 subgroup.add_argument( 

138 '-e', 

139 '--enable', 

140 dest='enable', 

141 action='store_true', 

142 help='Enable pre-commit hooks', 

143 ) 

144 subgroup.add_argument( 

145 '-d', 

146 '--disable', 

147 dest='disable', 

148 action='store_true', 

149 help='Disable pre-commit hooks', 

150 ) 

151 subgroup.add_argument( 

152 '-a', 

153 '--autoupdate', 

154 dest='autoupdate', 

155 action='store_true', 

156 help='Autoupdate pre-commit hooks', 

157 ) 

158 subgroup.add_argument( 

159 '-C', 

160 '--clean', 

161 dest='clean', 

162 action='store_true', 

163 help='Clean pre-commit cached hooks', 

164 ) 

165 subgroup.add_argument( 

166 '-r', 

167 '--run', 

168 dest='run', 

169 action='store_true', 

170 help='Run pre-commit hooks', 

171 ) 

172 

173 # Arguments configurations definitions 

174 group = parser.add_argument_group('configurations arguments') 

175 subgroup = group.add_mutually_exclusive_group() 

176 subgroup.add_argument( 

177 '--config', 

178 dest='config', 

179 action='store', 

180 metavar='FOLDER', 

181 help='Use configurations from a specific folder', 

182 ) 

183 subgroup.add_argument( 

184 '-D', 

185 '--default', 

186 dest='default', 

187 action='store_true', 

188 help='Use global default configurations instead of sources', 

189 ) 

190 group.add_argument( 

191 '--commit', 

192 dest='commit', 

193 action='store_true', 

194 help='Commit configurations changes automatically (implies --configure)', 

195 ) 

196 group.add_argument( 

197 '-p', 

198 '--patch', 

199 dest='patch', 

200 action='store_true', 

201 help='Add configurations changes interactively with \'git add --patch\'\n' \ 

202 '(implies --commit)', 

203 ) 

204 subgroup = group.add_mutually_exclusive_group() 

205 subgroup.add_argument( 

206 '--components', 

207 dest='components', 

208 action='store_true', 

209 help='Import components from GitLab with \'include: component:\'', 

210 ) 

211 subgroup.add_argument( 

212 '--remotes', 

213 dest='remotes', 

214 action='store_true', 

215 help='Import components from GitLab with \'include: remote:\'', 

216 ) 

217 subgroup.add_argument( 

218 '--no-components', 

219 dest='no_components', 

220 action='store_true', 

221 help='Import components templates locally instead of \'include: component:\'', 

222 ) 

223 group.add_argument( 

224 '--offline', 

225 dest='offline', 

226 action='store_true', 

227 help='Use offline mode to disable configurations autoupdate', 

228 ) 

229 group.add_argument( 

230 '--stage', 

231 dest='stage', 

232 action='store', 

233 metavar='STAGE', 

234 help='Run a specific pre-commit stage with --run\n' \ 

235 '(use \'list\' to list supported stages)\n' \ 

236 'or use a specific GitLab CI stage with --configure\n' \ 

237 '(default: test)', 

238 ) 

239 

240 # Arguments settings definitions 

241 group = parser.add_argument_group('settings arguments') 

242 group.add_argument( 

243 '--set-engine', 

244 dest='set_engine', 

245 metavar='ENGINE', 

246 type=lambda x: str(x) if str(x) in (PreCommit.ENGINE, Prek.ENGINE) else None, 

247 nargs='?', 

248 const=PreCommit.ENGINE, 

249 help='Set pre-commit engine to use' 

250 f' ({PreCommit.ENGINE}, {Prek.ENGINE}, default: %(const)s)', 

251 ) 

252 

253 # Arguments positional definitions 

254 group = parser.add_argument_group('positional arguments') 

255 group.add_argument( 

256 '--', 

257 dest='double_dash', 

258 action='store_true', 

259 help='Positional arguments separator (recommended)', 

260 ) 

261 

262 # Arguments parser 

263 options: Namespace = parser.parse_args() 

264 

265 # Help informations 

266 if options.help: 

267 print(' ') 

268 parser.print_help() 

269 print(' ') 

270 Platform.flush() 

271 sys_exit(0) 

272 

273 # Instantiate settings 

274 settings: Settings = Settings(name=Bundle.NAME) 

275 

276 # Prepare no_color 

277 if not options.no_color: 

278 if settings.has('themes', 'no_color'): 

279 options.no_color = settings.get_bool('themes', 'no_color') 

280 else: 

281 options.no_color = False 

282 settings.set_bool('themes', 'no_color', options.no_color) 

283 

284 # Configure no_color 

285 if options.no_color: 

286 environ[Bundle.ENV_FORCE_COLOR] = '0' 

287 environ[Bundle.ENV_NO_COLOR] = '1' 

288 

289 # Prepare colors 

290 Colors.prepare() 

291 

292 # Settings setter 

293 if options.set: 

294 settings.set(options.set[0], options.set[1], options.set[2]) 

295 settings.show() 

296 sys_exit(0) 

297 

298 # Settings informations 

299 if options.settings: 

300 settings.show() 

301 sys_exit(0) 

302 

303 # Instantiate updates 

304 updates: Updates = Updates( 

305 name=Bundle.PACKAGE, 

306 settings=settings, 

307 ) 

308 

309 # Version informations 

310 if options.version: 

311 print( 

312 f'{Bundle.NAME} {Version.get()} from {Version.path()} (python {Version.python()})' 

313 ) 

314 Platform.flush() 

315 sys_exit(0) 

316 

317 # Check for current updates 

318 if options.update_check: 

319 if not updates.check(): 

320 updates.check(older=True) 

321 sys_exit(0) 

322 

323 # Engine setter 

324 if options.set_engine: 

325 settings.set('engines', 'pre_commit', options.set_engine) 

326 settings.show() 

327 sys_exit(0) 

328 

329 # Implicit modes 

330 if options.patch: 

331 options.commit = True 

332 if options.commit and not options.configure and not options.badges: 

333 options.configure = True 

334 

335 # Validate stage usage 

336 if options.stage is not None: 

337 if options.configure: 

338 if options.stage == 'list' or not fullmatch( 

339 r'[A-Za-z0-9_.-]+( [A-Za-z0-9_.-]+)*', 

340 options.stage, 

341 ): 

342 parser.error(f'argument --stage: invalid GitLab CI stage name' 

343 f' \'{options.stage}\' with --configure') 

344 elif not options.run: 

345 parser.error('argument --stage: only supported with --run or --configure') 

346 

347 # Arguments modes 

348 if options.autoupdate: 

349 options.enable = True 

350 elif not ( \ 

351 options.list or \ 

352 options.install or \ 

353 options.configure or \ 

354 options.badges or \ 

355 options.enable or \ 

356 options.disable or \ 

357 options.clean or \ 

358 options.run \ 

359 ): 

360 result = Entrypoint.Result.CRITICAL 

361 

362 # Header 

363 print(' ') 

364 Platform.flush() 

365 

366 # Tool identifier 

367 if result != Entrypoint.Result.CRITICAL: 

368 print(f'{Colors.BOLD} {Bundle.NAME}' 

369 f'{Colors.YELLOW_LIGHT} ({Version.get()})' 

370 f'{Colors.RESET}') 

371 Platform.flush() 

372 

373 # CLI entrypoint 

374 if result != Entrypoint.Result.CRITICAL: 

375 result = Entrypoint.cli(options) 

376 

377 # CLI helper 

378 else: 

379 parser.print_help() 

380 

381 # Footer 

382 print(' ') 

383 Platform.flush() 

384 

385 # Check for daily updates 

386 if updates.enabled and updates.daily: 

387 updates.check() 

388 

389 # Result 

390 if result in [ 

391 Entrypoint.Result.SUCCESS, 

392 Entrypoint.Result.FINALIZE, 

393 ]: 

394 sys_exit(0) 

395 else: 

396 sys_exit(1) 

397 

398# Entrypoint 

399if __name__ == '__main__': # pragma: no cover 

400 main()