Coverage for pyVersioning/CLI.py: 74%
243 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-05-30 22:21 +0000
« 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
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
45from pyVersioning import __version__, __author__, __email__, __copyright__, __license__
46from pyVersioning import Versioning, Platforms, Project, SelfDescriptive
47from pyVersioning.Configuration import Configuration
50@export
51class ProjectAttributeGroup(CommandGroupAttribute):
52 """
53 This attribute group applies the following ArgParse attributes:
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
66@export
67class CompilerAttributeGroup(CommandGroupAttribute):
68 """
69 This attribute group applies the following ArgParse attributes:
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
84ArgNames = namedtuple("ArgNames", ("Command", "Template", "Filename", "ProjectName", "ProjectVariant", "ProjectVersion", "CompilerName", "CompilerVersion", "CompilerConfig", "CompilerOptions"))
87@export
88class Application(TerminalApplication, ArgParseHelperMixin):
89 """
90 pyVersioning's command line interface application class.
91 """
92 HeadLine: str = "Version file generator."
94 __configFile: Path
95 _config: Nullable[Configuration]
96 _versioning: Nullable[Versioning]
98 def __init__(self) -> None:
99 super().__init__(Mode.TextToStdOut_ErrorsToStdErr)
101 self.HeadLine = "Version file generator."
103 self.__configFile = Path(".pyVersioning.yml")
104 self._config = None
105 self._versioning = None
107 ArgParseHelperMixin.__init__(
108 self,
109 description=self.HeadLine,
110 formatter_class=RawDescriptionHelpFormatter,
111 add_help=False
112 )
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}"
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()
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()
141 def Run(self) -> NoReturn:
142 super().Run() # todo: enableAutoComplete ??
143 self.Exit()
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))
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__}")
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)
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("")
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.")
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()
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)
205 self.UpdateProject(args)
206 self.UpdateCompiler(args)
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)
215 for key,value in self._versioning.Variables.items():
216 _print(key, value, 0)
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))
229 query = f"{ {args.Field}} "
231 content = self.FillOutTemplate(query)
233 self.WriteOutput(
234 None if args.Filename is None else Path(args.Filename),
235 content
236 )
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))
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.")
253 template = templateFile.read_text(encoding="utf-8")
255 self.UpdateProject(args)
256 self.UpdateCompiler(args)
258 content = self.FillOutTemplate(template)
260 self.WriteOutput(
261 None if args.Filename is None else Path(args.Filename),
262 content
263 )
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)
274 self.UpdateProject(args)
275 self.UpdateCompiler(args)
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 """)
290 content = self._versioning.FillOutTemplate(template)
292 self.WriteOutput(
293 None if args.Filename is None else Path(args.Filename),
294 content
295 )
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)
306 self.UpdateProject(args)
307 self.UpdateCompiler(args)
309 yamlEnvironment = "\n"
310 # for key, value in self._versioning.variables["env"].as_dict().items():
311 # yamlEnvironment += f" {key}: {value}\n"
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"
334 template = dedent("""\
335 format: "1.1"
337 version:
338 name: "{version!s}"
339 major: {version.Major}
340 minor: {version.Minor}
341 patch: {version.Patch}
342 flags: {version.Flags}
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}
353 project:
354 name: {project.name}
355 variant: {project.variant}
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}
365 platform:
366 ci-service: {platform.ci_service}
367 appveyor: {yamlAppVeyor}
368 github: {yamlGitHub}
369 gitlab: {yamlGitLab}
370 travis: {yamlTravis}
371 env:{yamlEnvironment}
372 """)
374 content = self.FillOutTemplate(
375 template,
376 yamlEnvironment=yamlEnvironment,
377 yamlAppVeyor=yamlAppVeyor,
378 yamlGitHub=yamlGitHub,
379 yamlGitLab=yamlGitLab,
380 yamlTravis=yamlTravis
381 )
383 self.WriteOutput(
384 None if args.Filename is None else Path(args.Filename),
385 content
386 )
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
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
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
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
410 def FillOutTemplate(self, template: str, **kwargs) -> str:
411 self.WriteVerbose(f"Applying variables to template ...")
412 return self._versioning.FillOutTemplate(template, **kwargs)
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.")
426 self.ExitOnPreviousErrors()
428 outputFile.write_text(content, encoding="utf-8")
429 else:
430 self.WriteToStdOut(content)
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)
445if __name__ == "__main__": 445 ↛ 446line 445 didn't jump to line 446 because the condition on line 445 was never true
446 main()