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
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-08 13:49 +0000
1#!/usr/bin/env python3
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
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
28# Constants
29HELP_POSITION: int = 25
31# Main, pylint: disable=too-many-branches,too-many-statements
32def main() -> None:
34 # Variables
35 environments: Environments
36 group: _ArgumentGroup
37 result: Entrypoint.Result = Entrypoint.Result.ERROR
38 subgroup: _MutuallyExclusiveGroup
40 # Configure environment variables
41 environments = Environments()
43 # Configure pre-commit
44 PreCommit.configure()
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 )
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 )
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 )
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 )
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 )
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 )
262 # Arguments parser
263 options: Namespace = parser.parse_args()
265 # Help informations
266 if options.help:
267 print(' ')
268 parser.print_help()
269 print(' ')
270 Platform.flush()
271 sys_exit(0)
273 # Instantiate settings
274 settings: Settings = Settings(name=Bundle.NAME)
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)
284 # Configure no_color
285 if options.no_color:
286 environ[Bundle.ENV_FORCE_COLOR] = '0'
287 environ[Bundle.ENV_NO_COLOR] = '1'
289 # Prepare colors
290 Colors.prepare()
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)
298 # Settings informations
299 if options.settings:
300 settings.show()
301 sys_exit(0)
303 # Instantiate updates
304 updates: Updates = Updates(
305 name=Bundle.PACKAGE,
306 settings=settings,
307 )
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)
317 # Check for current updates
318 if options.update_check:
319 if not updates.check():
320 updates.check(older=True)
321 sys_exit(0)
323 # Engine setter
324 if options.set_engine:
325 settings.set('engines', 'pre_commit', options.set_engine)
326 settings.show()
327 sys_exit(0)
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
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')
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
362 # Header
363 print(' ')
364 Platform.flush()
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()
373 # CLI entrypoint
374 if result != Entrypoint.Result.CRITICAL:
375 result = Entrypoint.cli(options)
377 # CLI helper
378 else:
379 parser.print_help()
381 # Footer
382 print(' ')
383 Platform.flush()
385 # Check for daily updates
386 if updates.enabled and updates.daily:
387 updates.check()
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)
398# Entrypoint
399if __name__ == '__main__': # pragma: no cover
400 main()