Coverage for pyVersioning/CLI.py: 74%

243 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-05-30 22:21 +0000

1# ==================================================================================================================== # 

2# __ __ _ _ # 

3# _ __ _ \ \ / /__ _ __ ___(_) ___ _ __ (_)_ __ __ _ # 

4# | '_ \| | | \ \ / / _ \ '__/ __| |/ _ \| '_ \| | '_ \ / _` | # 

5# | |_) | |_| |\ V / __/ | \__ \ | (_) | | | | | | | | (_| | # 

6# | .__/ \__, | \_/ \___|_| |___/_|\___/|_| |_|_|_| |_|\__, | # 

7# |_| |___/ |___/ # 

8# ==================================================================================================================== # 

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

13# ==================================================================================================================== # 

14# Copyright 2020-2025 Patrick Lehmann - Bötzingen, Germany # 

15# # 

16# Licensed under the Apache License, Version 2.0 (the "License"); # 

17# you may not use this file except in compliance with the License. # 

18# You may obtain a copy of the License at # 

19# # 

20# http://www.apache.org/licenses/LICENSE-2.0 # 

21# # 

22# Unless required by applicable law or agreed to in writing, software # 

23# distributed under the License is distributed on an "AS IS" BASIS, # 

24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

25# See the License for the specific language governing permissions and # 

26# limitations under the License. # 

27# # 

28# SPDX-License-Identifier: Apache-2.0 # 

29# ==================================================================================================================== # 

30# 

31from argparse import RawDescriptionHelpFormatter, Namespace 

32from collections import namedtuple 

33from pathlib import Path 

34from textwrap import dedent 

35from typing import NoReturn, Optional as Nullable 

36 

37from pyTooling.Attributes import Entity 

38from pyTooling.Decorators import export 

39from pyTooling.Attributes.ArgParse import ArgParseHelperMixin, DefaultHandler, CommandHandler, CommandGroupAttribute 

40from pyTooling.Attributes.ArgParse.Argument import StringArgument, PathArgument 

41from pyTooling.Attributes.ArgParse.Flag import FlagArgument 

42from pyTooling.Attributes.ArgParse.ValuedFlag import LongValuedFlag 

43from pyTooling.TerminalUI import TerminalApplication, Severity, Mode 

44 

45from pyVersioning import __version__, __author__, __email__, __copyright__, __license__ 

46from pyVersioning import Versioning, Platforms, Project, SelfDescriptive 

47from pyVersioning.Configuration import Configuration 

48 

49 

50@export 

51class ProjectAttributeGroup(CommandGroupAttribute): 

52 """ 

53 This attribute group applies the following ArgParse attributes: 

54 

55 * ``--project-name`` 

56 * ``--project-variant`` 

57 * ``--project-version`` 

58 """ 

59 def __call__(self, func: Entity) -> Entity: 

60 self._AppendAttribute(func, LongValuedFlag("--project-name", dest="ProjectName", metaName="<Name>", optional=True, help="Name of the project.")) 

61 self._AppendAttribute(func, LongValuedFlag("--project-variant", dest="ProjectVariant", metaName="<Variant>", optional=True, help="Variant of the project.")) 

62 self._AppendAttribute(func, LongValuedFlag("--project-version", dest="ProjectVersion", metaName="<Version>", optional=True, help="Version of the project.")) 

63 return func 

64 

65 

66@export 

67class CompilerAttributeGroup(CommandGroupAttribute): 

68 """ 

69 This attribute group applies the following ArgParse attributes: 

70 

71 * ``--compiler-name`` 

72 * ``--compiler-version`` 

73 * ``--compiler-config`` 

74 * ``--compiler-options`` 

75 """ 

76 def __call__(self, func: Entity) -> Entity: 

77 self._AppendAttribute(func, LongValuedFlag("--compiler-name", dest="CompilerName", metaName="<Name>", optional=True, help="Used compiler.")) 

78 self._AppendAttribute(func, LongValuedFlag("--compiler-version", dest="CompilerVersion", metaName="<Version>", optional=True, help="Used compiler version.")) 

79 self._AppendAttribute(func, LongValuedFlag("--compiler-config", dest="CompilerConfig", metaName="<Config>", optional=True, help="Used compiler configuration.")) 

80 self._AppendAttribute(func, LongValuedFlag("--compiler-options", dest="CompilerOptions", metaName="<Options>", optional=True, help="Used compiler options.")) 

81 return func 

82 

83 

84ArgNames = namedtuple("ArgNames", ("Command", "Template", "Filename", "ProjectName", "ProjectVariant", "ProjectVersion", "CompilerName", "CompilerVersion", "CompilerConfig", "CompilerOptions")) 

85 

86 

87@export 

88class Application(TerminalApplication, ArgParseHelperMixin): 

89 """ 

90 pyVersioning's command line interface application class. 

91 """ 

92 HeadLine: str = "Version file generator." 

93 

94 __configFile: Path 

95 _config: Nullable[Configuration] 

96 _versioning: Nullable[Versioning] 

97 

98 def __init__(self) -> None: 

99 super().__init__(Mode.TextToStdOut_ErrorsToStdErr) 

100 

101 self.HeadLine = "Version file generator." 

102 

103 self.__configFile = Path(".pyVersioning.yml") 

104 self._config = None 

105 self._versioning = None 

106 

107 ArgParseHelperMixin.__init__( 

108 self, 

109 description=self.HeadLine, 

110 formatter_class=RawDescriptionHelpFormatter, 

111 add_help=False 

112 ) 

113 

114 self._LOG_MESSAGE_FORMAT__[Severity.Fatal] = "{DARK_RED}[FATAL] {message}{NOCOLOR}" 

115 self._LOG_MESSAGE_FORMAT__[Severity.Error] = "{RED}[ERROR] {message}{NOCOLOR}" 

116 self._LOG_MESSAGE_FORMAT__[Severity.Warning] = "{YELLOW}[WARNING] {message}{NOCOLOR}" 

117 self._LOG_MESSAGE_FORMAT__[Severity.Normal]= "{GRAY}{message}{NOCOLOR}" 

118 

119 def Initialize(self, configFile: Nullable[Path] = None) -> None: 

120 if configFile is None: 

121 if not self.__configFile.exists(): 121 ↛ 125line 121 didn't jump to line 125 because the condition on line 121 was always true

122 self.WriteWarning(f"Configuration file '{self.__configFile}' does not exist.") 

123 self._config = Configuration() 

124 else: 

125 self.WriteVerbose(f"Reading configuration file '{self.__configFile}'") 

126 self._config = Configuration(self.__configFile) 

127 elif configFile.exists(): 

128 self.WriteVerbose(f"Reading configuration file '{configFile}'") 

129 self._config = Configuration(configFile) 

130 else: 

131 self.WriteError(f"Configuration file '{configFile}' does not exist.") 

132 self._config = Configuration() 

133 

134 self.WriteVerbose(f"Creating internal data model ...") 

135 self._versioning = Versioning(self) 

136 self.WriteDebug(f" Loading information from configuration file ...") 

137 self._versioning.LoadDataFromConfiguration(self._config) 

138 self.WriteDebug(f" Collecting information from environment ...") 

139 self._versioning.CollectData() 

140 

141 def Run(self) -> NoReturn: 

142 super().Run() # todo: enableAutoComplete ?? 

143 self.Exit() 

144 

145 def _PrintHeadline(self) -> None: 

146 """Helper method to print the program headline.""" 

147 self.WriteNormal("{HEADLINE}{line}".format(line="=" * 80, **TerminalApplication.Foreground)) 

148 self.WriteNormal("{HEADLINE}{headline: ^80s}".format(headline=self.HeadLine, **TerminalApplication.Foreground)) 

149 self.WriteNormal("{HEADLINE}{line}".format(line="=" * 80, **TerminalApplication.Foreground)) 

150 

151 def _PrintVersion(self) -> None: 

152 """Helper method to print the version information.""" 

153 self.WriteNormal(f"Author: {__author__} ({__email__})") 

154 self.WriteNormal(f"Copyright: {__copyright__}") 

155 self.WriteNormal(f"License: {__license__}") 

156 self.WriteNormal(f"Version: {__version__}") 

157 

158 @DefaultHandler() 

159 @FlagArgument(short="-v", long="--verbose", dest="Verbose", help="Print verbose messages.") 

160 @FlagArgument(short="-d", long="--debug", dest="Debug", help="Print debug messages.") 

161 @LongValuedFlag("--config-file", dest="ConfigFile", metaName="<pyVersioning.yaml>", optional=True, help="Path to pyVersioning.yaml .") 

162 def HandleDefault(self, args: Namespace) -> None: 

163 """Handle program calls for no given command.""" 

164 self.Configure(verbose=args.Verbose, debug=args.Debug) 

165 self._PrintHeadline() 

166 self._PrintVersion() 

167 self.WriteNormal("") 

168 self.MainParser.print_help(self._stdout) 

169 

170 @CommandHandler("help", help="Display help page(s) for the given command name.") 

171 @StringArgument(dest="Command", metaName="Command", optional=True, help="Print help page(s) for a command.") 

172 def HandleHelp(self, args: Namespace) -> None: 

173 """Handle program calls for command ``help``.""" 

174 self.Configure(verbose=args.Verbose, debug=args.Debug) 

175 self._PrintHeadline() 

176 self._PrintVersion() 

177 self.WriteNormal("") 

178 

179 if args.Command is None: 179 ↛ 181line 179 didn't jump to line 181 because the condition on line 179 was always true

180 self.MainParser.print_help(self._stdout) 

181 elif args.Command == "help": 

182 self.WriteError("This is a recursion ...") 

183 else: 

184 try: 

185 self.SubParsers[args.Command].print_help(self._stdout) 

186 except KeyError: 

187 self.WriteError(f"Command {args.Command} is unknown.") 

188 

189 @CommandHandler("version", help="Display version information.", description="Display version information.") 

190 def HandleVersion(self, args: Namespace) -> None: 

191 """Handle program calls for command ``version``.""" 

192 self.Configure(verbose=args.Verbose, debug=args.Debug) 

193 self._PrintHeadline() 

194 self._PrintVersion() 

195 

196 @CommandHandler("variables", help="Print all available variables.") 

197 @ProjectAttributeGroup("dummy") 

198 @CompilerAttributeGroup("flummy") 

199 def HandleVariables(self, args: Namespace) -> None: 

200 """Handle program calls for command ``variables``.""" 

201 self.Configure(verbose=args.Verbose, debug=args.Debug, quiet=True) 

202 self._PrintHeadline() 

203 self.Initialize(Path(args.ConfigFile) if args.ConfigFile is not None else None) 

204 

205 self.UpdateProject(args) 

206 self.UpdateCompiler(args) 

207 

208 def _print(key, value, indent): 

209 key = (" " * indent) + str(key) 

210 self.WriteQuiet(f"{key:24}: {value!s}") 

211 if isinstance(value, SelfDescriptive): 

212 for k, v in value.KeyValuePairs(): 

213 _print(k, v, indent + 1) 

214 

215 for key,value in self._versioning.Variables.items(): 

216 _print(key, value, 0) 

217 

218 @CommandHandler("field", help="Return a single pyVersioning field.") 

219 @ProjectAttributeGroup("dummy") 

220 @CompilerAttributeGroup("flummy") 

221 @StringArgument(dest="Field", metaName="<Field name>", help="Field to return.") 

222 @PathArgument(dest="Filename", metaName="<Output file>", optional=True, help="Output filename.") 

223 def HandleField(self, args: Namespace) -> None: 

224 """Handle program calls for command ``field``.""" 

225 self.Configure(verbose=args.Verbose, debug=args.Debug) #, quiet=args.Filename is None) 

226 self._PrintHeadline() 

227 self.Initialize(None if args.ConfigFile is None else Path(args.ConfigFile)) 

228 

229 query = f"{ {args.Field}} " 

230 

231 content = self.FillOutTemplate(query) 

232 

233 self.WriteOutput( 

234 None if args.Filename is None else Path(args.Filename), 

235 content 

236 ) 

237 

238 @CommandHandler("fillout", help="Read a template and replace tokens with version information.") 

239 @ProjectAttributeGroup("dummy") 

240 @CompilerAttributeGroup("flummy") 

241 @PathArgument(dest="Template", metaName="<Template file>", help="Template input filename.") 

242 @PathArgument(dest="Filename", metaName="<Output file>", optional=True, help="Output filename.") 

243 def HandleFillOut(self, args: Namespace) -> None: 

244 """Handle program calls for command ``fillout``.""" 

245 self.Configure(verbose=args.Verbose, debug=args.Debug, quiet=args.Filename is None) 

246 self._PrintHeadline() 

247 self.Initialize(None if args.ConfigFile is None else Path(args.ConfigFile)) 

248 

249 templateFile = Path(args.Template) 

250 if not templateFile.exists(): 250 ↛ 251line 250 didn't jump to line 251 because the condition on line 250 was never true

251 self.WriteError(f"Template file '{templateFile}' does not exist.") 

252 

253 template = templateFile.read_text(encoding="utf-8") 

254 

255 self.UpdateProject(args) 

256 self.UpdateCompiler(args) 

257 

258 content = self.FillOutTemplate(template) 

259 

260 self.WriteOutput( 

261 None if args.Filename is None else Path(args.Filename), 

262 content 

263 ) 

264 

265 @CommandHandler("json", help="Write all available variables as JSON.") 

266 @ProjectAttributeGroup("dummy") 

267 @CompilerAttributeGroup("flummy") 

268 @PathArgument(dest="Filename", metaName="<Output file>", optional=True, help="Output filename.") 

269 def HandleJSON(self, args: Namespace) -> None: 

270 """Handle program calls for command ``json``.""" 

271 self.Configure(verbose=args.Verbose, debug=args.Debug, quiet=args.Filename is None) 

272 self.Initialize(Path(args.ConfigFile) if args.ConfigFile is not None else None) 

273 

274 self.UpdateProject(args) 

275 self.UpdateCompiler(args) 

276 

277 template = dedent("""\ 

278 {{ 

279 "format": "1.1", 

280 "version": {{ 

281 "name": "{version!s}", 

282 "major": {version.Major}, 

283 "minor": {version.Minor}, 

284 "patch": {version.Patch}, 

285 "flags": {version.Flags.value} 

286 }} 

287 }} 

288 """) 

289 

290 content = self._versioning.FillOutTemplate(template) 

291 

292 self.WriteOutput( 

293 None if args.Filename is None else Path(args.Filename), 

294 content 

295 ) 

296 

297 @CommandHandler("yaml", help="Write all available variables as YAML.") 

298 @ProjectAttributeGroup("dummy") 

299 @CompilerAttributeGroup("flummy") 

300 @PathArgument(dest="Filename", metaName="<Output file>", optional=True, help="Output filename.") 

301 def HandleYAML(self, args: Namespace) -> None: 

302 """Handle program calls for command ``yaml``.""" 

303 self.Configure(verbose=args.Verbose, debug=args.Debug, quiet=args.Filename is None) 

304 self.Initialize(Path(args.ConfigFile) if args.ConfigFile is not None else None) 

305 

306 self.UpdateProject(args) 

307 self.UpdateCompiler(args) 

308 

309 yamlEnvironment = "\n" 

310 # for key, value in self._versioning.variables["env"].as_dict().items(): 

311 # yamlEnvironment += f" {key}: {value}\n" 

312 

313 yamlAppVeyor = "\n# not found" 

314 yamlGitHub = "\n# not found" 

315 yamlGitLab = "\n# not found" 

316 yamlTravis = "\n# not found" 

317 if self._versioning.Platform is Platforms.AppVeyor: 317 ↛ 318line 317 didn't jump to line 318 because the condition on line 317 was never true

318 yamlAppVeyor = "\n" 

319 for key, value in self._versioning.Variables["appveyor"].as_dict().items(): 

320 yamlAppVeyor += f" {key}: {value}\n" 

321 elif self._versioning.Platform is Platforms.GitHub: 321 ↛ 325line 321 didn't jump to line 325 because the condition on line 321 was always true

322 yamlGitHub = "\n" 

323 for key, value in self._versioning.Variables["github"].as_dict().items(): 

324 yamlGitHub += f" {key}: {value}\n" 

325 elif self._versioning.Platform is Platforms.GitLab: 

326 yamlGitLab = "\n" 

327 for key, value in self._versioning.Variables["gitlab"].as_dict().items(): 

328 yamlGitLab += f" {key}: {value}\n" 

329 elif self._versioning.Platform is Platforms.Travis: 

330 yamlTravis = "\n" 

331 for key, value in self._versioning.Variables["travis"].as_dict().items(): 

332 yamlTravis += f" {key}: {value}\n" 

333 

334 template = dedent("""\ 

335 format: "1.1" 

336 

337 version: 

338 name: "{version!s}" 

339 major: {version.Major} 

340 minor: {version.Minor} 

341 patch: {version.Patch} 

342 flags: {version.Flags} 

343 

344 git: 

345 commit: 

346 hash: {git.commit.hash} 

347 date: {git.commit.date} 

348 reference: {git.reference} 

349 branch: {git.branch} 

350 tag: {git.tag} 

351 repository: {git.repository} 

352 

353 project: 

354 name: {project.name} 

355 variant: {project.variant} 

356 

357 build: 

358 date: {build.date} 

359 compiler: 

360 name: {build.compiler.name} 

361 version: {build.compiler.version} 

362 configuration: {build.compiler.configuration} 

363 options: {build.compiler.options} 

364 

365 platform: 

366 ci-service: {platform.ci_service} 

367 appveyor: {yamlAppVeyor} 

368 github: {yamlGitHub} 

369 gitlab: {yamlGitLab} 

370 travis: {yamlTravis} 

371 env:{yamlEnvironment} 

372 """) 

373 

374 content = self.FillOutTemplate( 

375 template, 

376 yamlEnvironment=yamlEnvironment, 

377 yamlAppVeyor=yamlAppVeyor, 

378 yamlGitHub=yamlGitHub, 

379 yamlGitLab=yamlGitLab, 

380 yamlTravis=yamlTravis 

381 ) 

382 

383 self.WriteOutput( 

384 None if args.Filename is None else Path(args.Filename), 

385 content 

386 ) 

387 

388 def UpdateProject(self, args: Namespace) -> None: 

389 if "project" not in self._versioning.Variables: 389 ↛ 390line 389 didn't jump to line 390 because the condition on line 389 was never true

390 self._versioning.Variables["project"] = Project(args.ProjectName, args.ProjectVersion, args.ProjectVariant) 

391 elif args.ProjectName is not None: 391 ↛ 392line 391 didn't jump to line 392 because the condition on line 391 was never true

392 self._versioning.Variables["project"]._name = args.ProjectName 

393 

394 if args.ProjectVariant is not None: 394 ↛ 395line 394 didn't jump to line 395 because the condition on line 394 was never true

395 self._versioning.Variables["project"]._variant = args.ProjectVariant 

396 

397 if args.ProjectVersion is not None: 397 ↛ 398line 397 didn't jump to line 398 because the condition on line 397 was never true

398 self._versioning.Variables["project"]._version = args.ProjectVersion 

399 

400 def UpdateCompiler(self, args: Namespace) -> None: 

401 if args.CompilerName is not None: 401 ↛ 402line 401 didn't jump to line 402 because the condition on line 401 was never true

402 self._versioning.Variables["build"]._compiler._name = args.CompilerName 

403 if args.CompilerVersion is not None: 403 ↛ 404line 403 didn't jump to line 404 because the condition on line 403 was never true

404 self._versioning.Variables["build"]._compiler._version = args.CompilerVersion 

405 if args.CompilerConfig is not None: 405 ↛ 406line 405 didn't jump to line 406 because the condition on line 405 was never true

406 self._versioning.Variables["build"]._compiler._configuration = args.CompilerConfig 

407 if args.CompilerOptions is not None: 407 ↛ 408line 407 didn't jump to line 408 because the condition on line 407 was never true

408 self._versioning.Variables["build"]._compiler._options = args.CompilerOptions 

409 

410 def FillOutTemplate(self, template: str, **kwargs) -> str: 

411 self.WriteVerbose(f"Applying variables to template ...") 

412 return self._versioning.FillOutTemplate(template, **kwargs) 

413 

414 def WriteOutput(self, outputFile: Nullable[Path], content: str): 

415 if outputFile is not None: 415 ↛ 416line 415 didn't jump to line 416 because the condition on line 415 was never true

416 self.WriteVerbose(f"Writing output to '{outputFile}' ...") 

417 if not outputFile.parent.exists(): 

418 self.WriteWarning(f"Directory for file '{outputFile}' does not exist. Directory will be created") 

419 try: 

420 outputFile.parent.mkdir() 

421 except: 

422 self.WriteError(f"Failed to create the directory '{outputFile.parent}' for the output file.") 

423 elif outputFile.exists(): 

424 self.WriteWarning(f"Output file '{outputFile}' already exists. This file will be overwritten.") 

425 

426 self.ExitOnPreviousErrors() 

427 

428 outputFile.write_text(content, encoding="utf-8") 

429 else: 

430 self.WriteToStdOut(content) 

431 

432 

433def main() -> NoReturn: 

434 """Entrypoint for program execution.""" 

435 application = Application() 

436 application.CheckPythonVersion((3, 8, 0)) 

437 try: 

438 application.Run() 

439 # except ServiceException as ex: 

440 # application.PrintException(ex) 

441 except Exception as ex: 

442 application.PrintException(ex) 

443 

444 

445if __name__ == "__main__": 445 ↛ 446line 445 didn't jump to line 446 because the condition on line 445 was never true

446 main()