Coverage for pyVersioning / __init__.py: 71%
461 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-13 23:48 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-13 23:48 +0000
1# ==================================================================================================================== #
2# __ __ _ _ #
3# _ __ _ \ \ / /__ _ __ ___(_) ___ _ __ (_)_ __ __ _ #
4# | '_ \| | | \ \ / / _ \ '__/ __| |/ _ \| '_ \| | '_ \ / _` | #
5# | |_) | |_| |\ V / __/ | \__ \ | (_) | | | | | | | | (_| | #
6# | .__/ \__, | \_/ \___|_| |___/_|\___/|_| |_|_|_| |_|\__, | #
7# |_| |___/ |___/ #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2020-2026 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#
31__author__ = "Patrick Lehmann"
32__email__ = "Paebbels@gmail.com"
33__copyright__ = "2020-2026, Patrick Lehmann"
34__license__ = "Apache License, Version 2.0"
35__version__ = "0.18.4"
36__keywords__ = ["Python3", "Template", "Versioning", "Git"]
38from dataclasses import make_dataclass
39from datetime import date, time, datetime
40from enum import Enum, auto
41from os import environ
42from subprocess import run as subprocess_run, PIPE, CalledProcessError
43from typing import Union, Any, Dict, Tuple, ClassVar, Generator, Optional as Nullable, List
45from pyTooling.Decorators import export, readonly
46from pyTooling.MetaClasses import ExtendedType
47from pyTooling.Versioning import SemanticVersion
48from pyTooling.TerminalUI import ILineTerminal
50from pyVersioning.Configuration import Configuration, Project, Compiler, Build
53@export
54class VersioningException(Exception):
55 """Base-exception for all exceptions thrown by pyVersioning."""
58@export
59class ToolException(VersioningException):
60 """Exception thrown when a data collection tools has an error."""
62 command: str #: Command that caused the exception.
63 errorMessage: str #: Error message returned by the tool.
65 def __init__(self, command: str, errorMessage: str) -> None:
66 """
68 :param command: Command that caused the exception.
69 :param errorMessage: Error message returned by the tool.
70 """
71 super().__init__(errorMessage)
72 self.command = command
73 self.errorMessage = errorMessage
76@export
77class SelfDescriptive(metaclass=ExtendedType, slots=True, mixin=True):
78 # TODO: could this be filled with a decorator?
79 _public: ClassVar[Tuple[str, ...]]
81 def Keys(self) -> Generator[str, None, None]:
82 for element in self._public:
83 yield element
85 def KeyValuePairs(self) -> Generator[Tuple[str, Any], None, None]:
86 for element in self._public:
87 value = self.__getattribute__(element)
88 yield element, value
91@export
92class Tool(SelfDescriptive):
93 """This data structure class describes the tool name and version of pyVersioning."""
95 _name: str #: Name of pyVersioning
96 _version: SemanticVersion #: Version of the pyVersioning
98 _public: ClassVar[Tuple[str, ...]] = ("name", "version")
100 def __init__(self, name: str, version: SemanticVersion) -> None:
101 """
102 Initialize a tool with name and version.
104 :param name: The tool's name.
105 :param version: The tool's version.
106 """
107 self._name = name
108 self._version = version
110 @readonly
111 def name(self) -> str:
112 """
113 Read-only property to return the name of the tool.
115 :return: Name of the tool.
116 """
117 return self._name
119 @readonly
120 def version(self) -> SemanticVersion:
121 """
122 Read-only property to return the version of the tool.
124 :return: Version of the tool as :class:`SemanticVersion`.
125 """
126 return self._version
128 def __str__(self) -> str:
129 """
130 Return a string representation of this tool description.
132 :returns: The tool's name and version.
133 """
134 return f"{self._name} - {self._version}"
137@export
138class Date(date, SelfDescriptive):
139 """
140 This data structure class describes a date (dd.mm.yyyy).
141 """
143 _public: ClassVar[Tuple[str, ...]] = ("day", "month", "year")
146@export
147class Time(time, SelfDescriptive):
148 """
149 This data structure class describes a time (hh:mm:ss).
150 """
152 _public: ClassVar[Tuple[str, ...]] = ("hour", "minute", "second")
155@export
156class Person(SelfDescriptive):
157 """
158 This data structure class describes a person with name and email address.
160 This class is used to describe an author or committer in a version control system.
161 """
163 _name: str #: Name of the person.
164 _email: str #: Email address of the person.
166 _public: ClassVar[Tuple[str, ...]] = ("name", "email")
168 def __init__(self, name: str, email: str) -> None:
169 """
170 Initialize a person with name and email address.
172 :param name: The person's name.
173 :param email: The person's email address.
174 """
175 self._name = name
176 self._email = email
178 @readonly
179 def name(self) -> str:
180 """
181 Read-only property to return the name of the person.
183 :return: Name of the person.
184 """
185 return self._name
187 @readonly
188 def email(self) -> str:
189 """
190 Read-only property to return the email address of the person.
192 :return: Email address of the person.
193 """
194 return self._email
196 def __str__(self) -> str:
197 """
198 Return a string representation of a person (committer, author, ...).
200 :returns: The persons name and email address.
201 """
202 return f"{self._name} <{self._email}>"
205@export
206class Commit(SelfDescriptive):
207 """
208 This data structure class describes a Git commit with Hash, commit date and time, committer, author and commit
209 description.
210 """
212 _hash: str
213 _date: date
214 _time: time
215 _author: Person
216 _committer: Person
217 _comment: str
218 _oneline: Union[str, bool] = False
220 _public: ClassVar[Tuple[str, ...]] = ("hash", "date", "time", "author", "committer", "comment", "oneline")
222 def __init__(self, hash: str, date: date, time: time, author: Person, committer: Person, comment: str) -> None:
223 """
224 Initialize a Git commit.
226 :param hash: The commit's hash.
227 :param date: The commit's date.
228 :param time: The commit's time.
229 :param author: The commit's author.
230 :param committer: The commit's committer.
231 :param comment: The commit's comment.
232 """
233 self._hash = hash
234 self._date = date
235 self._time = time
236 self._author = author
237 self._committer = committer
238 self._comment = comment
240 if comment != "": 240 ↛ exitline 240 didn't return from function '__init__' because the condition on line 240 was always true
241 self._oneline = comment.split("\n")[0]
243 @readonly
244 def hash(self) -> str:
245 """
246 Read-only property to return the hash of the commit.
248 :return: Hash of the commit as string.
249 """
250 return self._hash
252 @readonly
253 def date(self) -> date:
254 """
255 Read-only property to return the date of the commit.
257 :return: Date of the commit as :class:`date`.
258 """
259 return self._date
261 @readonly
262 def time(self) -> time:
263 """
264 Read-only property to return the time of the commit.
266 :return: Time of the commit as :class:`time`.
267 """
268 return self._time
270 @readonly
271 def author(self) -> Person:
272 """
273 Read-only property to return the author of the commit.
275 :return: Author of the commit as :class:`Person`.
276 """
277 return self._author
279 @readonly
280 def committer(self) -> Person:
281 """
282 Read-only property to return the committer of the commit.
284 :return: Committer of the commit as :class:`Person`.
285 """
286 return self._committer
288 @readonly
289 def comment(self) -> str:
290 """
291 Read-only property to return the comment of the commit.
293 :return: Author of the comment as string.
294 """
295 return self._comment
297 @readonly
298 def oneline(self) -> Union[str, bool]:
299 return self._oneline
301 def __str__(self) -> str:
302 """
303 Return a string representation of a commit.
305 :returns: The date, time, committer, hash and one-liner description.
306 """
307 return f"{self._date} {self._time} - {self._committer} - {self._hash} - {self._oneline}"
310@export
311class Git(SelfDescriptive):
312 """
313 This data structure class describes all collected data from Git.
314 """
315 _commit: Commit #: Git commit information
316 _reference: str #: Git reference (branch or tag)
317 _tag: str #: Git tag
318 _branch: str #: Git branch
319 _repository: str #: Git repository URL
321 _public: ClassVar[Tuple[str, ...]] = ("commit", "reference", "tag", "branch", "repository")
323 def __init__(self, commit: Commit, repository: str, tag: str = "", branch: str = "") -> None:
324 self._commit = commit
325 self._tag = tag
326 self._branch = branch
327 self._repository = repository
329 if tag != "": 329 ↛ 331line 329 didn't jump to line 331 because the condition on line 329 was always true
330 self._reference = tag
331 elif branch != "":
332 self._reference = branch
333 else:
334 self._reference = "[Detached HEAD]"
336 @readonly
337 def commit(self) -> Commit:
338 """
339 Read-only property to return the Git commit.
341 :return: The commit information as :class:`Commit`.
342 """
343 return self._commit
345 @readonly
346 def reference(self) -> str:
347 """
348 Read-only property to return the Git reference.
350 A reference is either a branch or tag.
352 :return: The current Git reference as string.
353 """
354 return self._reference
356 @readonly
357 def tag(self) -> str:
358 """
359 Read-only property to return the Git tag.
361 :return: The current Git tag name as string.
362 """
363 return self._tag
365 @readonly
366 def branch(self) -> str:
367 """
368 Read-only property to return the Git branch.
370 :return: The current Git branch name as string.
371 """
372 return self._branch
374 @readonly
375 def repository(self) -> str:
376 """
377 Read-only property to return the Git repository URL.
379 :return: The current Git repository URL as string.
380 """
381 return self._repository
383 def __str__(self) -> str:
384 """
385 Return a string representation of a repository.
387 :returns: The date, time, committer, hash and one-liner description.
388 """
389 return f"{self._repository}:{self._reference} - {self._commit._hash} - {self._commit._oneline}"
392@export
393class Project(SelfDescriptive):
394 """This data structure class describes a project."""
396 _name: str #: Name of the project.
397 _variant: str #: Variant of the project.
398 _version: SemanticVersion #: Version of the project.
400 _public: ClassVar[Tuple[str, ...]] = ("name", "variant", "version")
402 def __init__(
403 self,
404 name: str,
405 version: Union[str, SemanticVersion, None] = None,
406 variant: Nullable[str] = None
407 ) -> None:
408 """Assign fields and convert version string to a `Version` object."""
410 self._name = name if name is not None else ""
411 self._variant = variant if variant is not None else ""
413 if isinstance(version, SemanticVersion):
414 self._version = version
415 elif isinstance(version, str):
416 self._version = SemanticVersion.Parse(version)
417 elif version is None: 417 ↛ exitline 417 didn't return from function '__init__' because the condition on line 417 was always true
418 self._version = SemanticVersion.Parse("v0.0.0")
420 @readonly
421 def name(self) -> str:
422 """
423 Read-only property to return the name of the project.
425 :return: The project's name as string.
426 """
427 return self._name
429 @readonly
430 def variant(self) -> str:
431 """
432 Read-only property to return the variant name of the project.
434 :return: The project's variant as string.
435 """
436 return self._variant
438 @readonly
439 def version(self) -> SemanticVersion:
440 """
441 Read-only property to return the version of the project.
443 :return: The project's version.
444 """
445 return self._version
447 def __str__(self) -> str:
448 """
449 Return a string representation of a project.
451 :returns: The project name, project variant and project version.
452 """
453 return f"{self._name} - {self._variant} {self._version}"
456@export
457class Compiler(SelfDescriptive):
458 _name: str #: Name of the compiler.
459 _version: SemanticVersion #: Version of the compiler.
460 _configuration: str #: Compiler configuration.
461 _options: str #: Compiler options (compiler flags).
463 _public: ClassVar[Tuple[str, ...]] = ("name", "version", "configuration", "options")
465 def __init__(
466 self,
467 name: str,
468 version: Union[str, SemanticVersion] = "",
469 configuration: str = "",
470 options: str = ""
471 ) -> None:
472 """Assign fields and convert version string to a `Version` object."""
474 self._name = name if name is not None else ""
475 self._configuration = configuration if configuration is not None else ""
476 self._options = options if options is not None else ""
478 if isinstance(version, SemanticVersion): 478 ↛ 480line 478 didn't jump to line 480 because the condition on line 478 was always true
479 self._version = version
480 elif isinstance(version, str):
481 self._version = SemanticVersion.Parse(version)
482 elif version is None:
483 self._version = SemanticVersion.Parse("v0.0.0")
485 @readonly
486 def name(self) -> str:
487 """
488 Read-only property to return the name of the compiler.
490 :return: The compiler's name as string.
491 """
492 return self._name
494 @readonly
495 def version(self) -> SemanticVersion:
496 """
497 Read-only property to return the version of the compiler.
499 :return: The compiler's version.
500 """
501 return self._version
503 @readonly
504 def configuration(self) -> str:
505 """
506 Read-only property to return the configuration of the compiler.
508 :return: The compiler's configuration.
509 """
510 return self._configuration
512 @readonly
513 def options(self) -> str:
514 """
515 Read-only property to return the options of the compiler.
517 :return: The compiler's options.
518 """
519 return self._options
521 def __str__(self) -> str:
522 """
523 Return a string representation of a compiler.
525 :returns: The compiler name and compiler version.
526 """
527 return f"{self._name} - {self._version}"
530@export
531class Build(SelfDescriptive):
532 _date: date #: Build date.
533 _time: time #: Build time.
534 _compiler: Compiler #: Use compiler (and options) for the build.
536 _public: ClassVar[Tuple[str, ...]] = ("date", "time", "compiler")
538 def __init__(self, date: date, time: time, compiler: Compiler) -> None:
539 self._date = date
540 self._time = time
541 self._compiler = compiler
543 @readonly
544 def date(self) -> date:
545 """
546 Read-only property to return the date of the build.
548 :return: Date of the build as :class:`date`.
549 """
550 return self._date
552 @readonly
553 def time(self) -> time:
554 """
555 Read-only property to return the time of the build.
557 :return: Time of the build as :class:`date`.
558 """
559 return self._time
561 @readonly
562 def compiler(self) -> Compiler:
563 return self._compiler
566@export
567class Platforms(Enum):
568 """An enumeration of platforms supported by pyTooling."""
569 Workstation = auto() #: A local work station, server, PC or laptop.
570 AppVeyor = auto() #: A CI service operated by `AppVeyor <https://www.appveyor.com/>`__.
571 GitHub = auto() #: A CI service offered by `GitHub <https://github.com/>`__ called GitHub Actions.
572 GitLab = auto() #: A CI service offered by `GitLab <https://about.gitlab.com/>`__.
573 Travis = auto() #: A CI service operated by `Travis <https://www.travis-ci.com/>`__.
576@export
577class Platform(SelfDescriptive):
578 """.. todo:: Platform needs documentation"""
580 _ciService: str
581 _public: ClassVar[Tuple[str, ...]] = ('ci_service', )
583 def __init__(self, ciService: str) -> None:
584 self._ciService = ciService
586 @readonly
587 def ci_service(self) -> str:
588 return self._ciService
591@export
592class BaseService(metaclass=ExtendedType):
593 """Base-class to collect platform and environment information from e.g. environment variables."""
595 # @abstractmethod
596 def GetPlatform(self) -> Platform: # type: ignore[empty-body]
597 """
598 .. todo::
599 getPlatform needs documentation
601 """
604@export
605class GitShowCommand(Enum):
606 CommitDateTime = auto()
607 CommitAuthorName = auto()
608 CommitAuthorEmail = auto()
609 CommitCommitterName = auto()
610 CommitCommitterEmail = auto()
611 CommitHash = auto()
612 CommitComment = auto()
615@export
616class GitHelperMixin(metaclass=ExtendedType, mixin=True):
617 __GIT_SHOW_COMMAND_TO_FORMAT_LOOKUP = {
618 GitShowCommand.CommitHash: "%H",
619 GitShowCommand.CommitDateTime: "%ct",
620 GitShowCommand.CommitAuthorName: "%an",
621 GitShowCommand.CommitAuthorEmail: "%ae",
622 GitShowCommand.CommitCommitterName: "%cn",
623 GitShowCommand.CommitCommitterEmail: "%ce",
624 GitShowCommand.CommitComment: "%B",
625 }
627 def ExecuteGitShow(self, cmd: GitShowCommand, ref: str = "HEAD") -> str:
628 format = f"--format='{self.__GIT_SHOW_COMMAND_TO_FORMAT_LOOKUP[cmd]}'"
630 command = "git"
631 arguments = ("show", "-s", format, ref)
632 try:
633 completed = subprocess_run((command, *arguments), stdout=PIPE, stderr=PIPE)
634 except CalledProcessError as ex:
635 raise ToolException(f"{command} {' '.join(arguments)}", str(ex))
637 if completed.returncode == 0: 637 ↛ 641line 637 didn't jump to line 641 because the condition on line 637 was always true
638 comment = completed.stdout.decode("utf-8")
639 return comment[1:-2]
640 else:
641 message = completed.stderr.decode("utf-8")
642 raise ToolException(f"{command} {' '.join(arguments)}", message)
645@export
646class Versioning(ILineTerminal, GitHelperMixin):
647 _variables: Dict[str, Any]
648 _platform: Platforms = Platforms.Workstation
649 _service: BaseService
651 def __init__(self, terminal: ILineTerminal) -> None:
652 super().__init__(terminal)
654 self._variables = {
655 "tool": Tool("pyVersioning", SemanticVersion.Parse(f"v{__version__}"))
656 }
658 if "APPVEYOR" in environ: 658 ↛ 659line 658 didn't jump to line 659 because the condition on line 658 was never true
659 self._platform = Platforms.AppVeyor
660 elif "GITHUB_ACTIONS" in environ: 660 ↛ 662line 660 didn't jump to line 662 because the condition on line 660 was always true
661 self._platform = Platforms.GitHub
662 elif "GITLAB_CI" in environ:
663 self._platform = Platforms.GitLab
664 elif "TRAVIS" in environ:
665 self._platform = Platforms.Travis
666 else:
667 self._platform = Platforms.Workstation
669 self.WriteDebug(f"Detected platform: {self._platform.name}")
671 @readonly
672 def Variables(self) -> Dict[str, Any]:
673 return self._variables
675 @readonly
676 def Platform(self) -> Platforms:
677 return self._platform
679 def LoadDataFromConfiguration(self, config: Configuration) -> None:
680 """Preload versioning information from a configuration file."""
682 self._variables["version"] = self.GetVersion(config.project)
683 self._variables["project"] = self.GetProject(config.project)
684 self._variables["build"] = self.GetBuild(config.build)
686 def CollectData(self) -> None:
687 """Collect versioning information from environment including CI services (if available)."""
689 from pyVersioning.AppVeyor import AppVeyor
690 from pyVersioning.CIService import WorkStation
691 from pyVersioning.GitLab import GitLab
692 from pyVersioning.GitHub import GitHub
693 from pyVersioning.Travis import Travis
695 if self._platform is Platforms.AppVeyor: 695 ↛ 696line 695 didn't jump to line 696 because the condition on line 695 was never true
696 self._service = AppVeyor()
697 self._variables["appveyor"] = self._service.GetEnvironment()
698 elif self._platform is Platforms.GitHub: 698 ↛ 701line 698 didn't jump to line 701 because the condition on line 698 was always true
699 self._service = GitHub()
700 self._variables["github"] = self._service.GetEnvironment()
701 elif self._platform is Platforms.GitLab:
702 self._service = GitLab()
703 self._variables["gitlab"] = self._service.GetEnvironment()
704 elif self._platform is Platforms.Travis:
705 self._service = Travis()
706 self._variables["travis"] = self._service.GetEnvironment()
707 else:
708 self._service = WorkStation()
710 self._variables["git"] = self.GetGitInformation()
711 self._variables["platform"] = self._service.GetPlatform()
712 self._variables["env"] = self.GetEnvironment()
714 self.CalculateData()
716 def CalculateData(self) -> None:
717 if self._variables["git"].tag != "": 717 ↛ exitline 717 didn't return from function 'CalculateData' because the condition on line 717 was always true
718 pass
720 def GetVersion(self, config: Project) -> SemanticVersion:
721 if config.version is not None: 721 ↛ 724line 721 didn't jump to line 724 because the condition on line 721 was always true
722 return config.version
723 else:
724 return SemanticVersion.Parse("0.0.0")
726 def GetGitInformation(self) -> Git:
727 return Git(
728 commit=self.GetLastCommit(),
729 tag=self.GetGitTag(),
730 branch=self.GetGitLocalBranch(),
731 repository=self.GetGitRemoteURL()
732 )
734 def GetLastCommit(self) -> Commit:
735 dt = self.GetCommitDate()
737 return Commit(
738 hash=self.GetGitHash(),
739 date=dt.date(),
740 time=dt.time(),
741 author=self.GetCommitAuthor(),
742 committer=self.GetCommitCommitter(),
743 comment=self.GetCommitComment()
744 )
746 def GetGitHash(self) -> str:
747 if self._platform is not Platforms.Workstation: 747 ↛ 750line 747 didn't jump to line 750 because the condition on line 747 was always true
748 return self._service.GetGitHash()
750 return self.ExecuteGitShow(GitShowCommand.CommitHash)
752 def GetCommitDate(self) -> datetime:
753 if self._platform is not Platforms.Workstation: 753 ↛ 756line 753 didn't jump to line 756 because the condition on line 753 was always true
754 return self._service.GetCommitDate()
756 datetimeString = self.ExecuteGitShow(GitShowCommand.CommitDateTime)
757 return datetime.fromtimestamp(int(datetimeString))
759 def GetCommitAuthor(self) -> Person:
760 return Person(
761 name=self.GetCommitAuthorName(),
762 email=self.GetCommitAuthorEmail()
763 )
765 def GetCommitAuthorName(self) -> str:
766 return self.ExecuteGitShow(GitShowCommand.CommitAuthorName)
768 def GetCommitAuthorEmail(self) -> str:
769 return self.ExecuteGitShow(GitShowCommand.CommitAuthorEmail)
771 def GetCommitCommitter(self) -> Person:
772 return Person(
773 name=self.GetCommitCommitterName(),
774 email=self.GetCommitCommitterEmail()
775 )
777 def GetCommitCommitterName(self) -> str:
778 return self.ExecuteGitShow(GitShowCommand.CommitCommitterName)
780 def GetCommitCommitterEmail(self) -> str:
781 return self.ExecuteGitShow(GitShowCommand.CommitCommitterEmail)
783 def GetCommitComment(self) -> str:
784 return self.ExecuteGitShow(GitShowCommand.CommitComment)
786 def GetGitLocalBranch(self) -> str:
787 if self._platform is not Platforms.Workstation: 787 ↛ 790line 787 didn't jump to line 790 because the condition on line 787 was always true
788 return self._service.GetGitBranch()
790 command = "git"
791 arguments = ("branch", "--show-current")
792 try:
793 completed = subprocess_run((command, *arguments), stdout=PIPE, stderr=PIPE)
794 except CalledProcessError:
795 return ""
797 if completed.returncode == 0:
798 return completed.stdout.decode("utf-8").split("\n")[0]
799 else:
800 message = completed.stderr.decode("utf-8")
801 self.WriteFatal(f"Message from '{command} {' '.join(arguments)}': {message}")
803 def GetGitRemoteBranch(self, localBranch: Nullable[str] = None) -> str:
804 if self._platform is not Platforms.Workstation:
805 return self._service.GetGitBranch()
807 if localBranch is None:
808 localBranch = self.GetGitLocalBranch()
810 command = "git"
811 arguments = ("config", f"branch.{localBranch}.merge")
812 try:
813 completed = subprocess_run((command, *arguments), stdout=PIPE, stderr=PIPE)
814 except CalledProcessError:
815 raise Exception() # XXX: needs error message
817 if completed.returncode == 0:
818 return completed.stdout.decode("utf-8").split("\n")[0]
819 else:
820 message = completed.stderr.decode("utf-8")
821 self.WriteFatal(f"Message from '{command} {' '.join(arguments)}': {message}")
822 raise Exception() # XXX: needs error message
824 def GetGitRemote(self, localBranch: Nullable[str] = None) -> str:
825 if localBranch is None:
826 localBranch = self.GetGitLocalBranch()
828 command = "git"
829 arguments = ("config", f"branch.{localBranch}.remote")
830 try:
831 completed = subprocess_run((command, *arguments), stdout=PIPE, stderr=PIPE)
832 except CalledProcessError:
833 raise Exception() # XXX: needs error message
835 if completed.returncode == 0:
836 return completed.stdout.decode("utf-8").split("\n")[0]
837 elif completed.returncode == 1:
838 self.WriteWarning(f"Branch '{localBranch}' is not pushed to a remote.")
839 return f"(local) {localBranch}"
840 else:
841 message = completed.stderr.decode("utf-8")
842 self.WriteFatal(f"Message from '{command} {' '.join(arguments)}': {message}")
843 raise Exception() # XXX: needs error message
845 def GetGitTag(self) -> str:
846 if self._platform is not Platforms.Workstation: 846 ↛ 849line 846 didn't jump to line 849 because the condition on line 846 was always true
847 return self._service.GetGitTag()
849 command = "git"
850 arguments = ("tag", "--points-at", "HEAD")
851 try:
852 completed = subprocess_run((command, *arguments), stdout=PIPE, stderr=PIPE)
853 except CalledProcessError:
854 raise Exception() # XXX: needs error message
856 if completed.returncode == 0:
857 return completed.stdout.decode("utf-8").split("\n")[0]
858 else:
859 message = completed.stderr.decode("utf-8")
860 self.WriteFatal(f"Message from '{command} {' '.join(arguments)}': {message}")
861 raise Exception() # XXX: needs error message
863 def GetGitRemoteURL(self, remote: Nullable[str] = None) -> str:
864 if self._platform is not Platforms.Workstation: 864 ↛ 867line 864 didn't jump to line 867 because the condition on line 864 was always true
865 return self._service.GetGitRepository()
867 if remote is None:
868 remote = self.GetGitRemote()
870 command = "git"
871 arguments = ("config", f"remote.{remote}.url")
872 try:
873 completed = subprocess_run((command, *arguments), stdout=PIPE, stderr=PIPE)
874 except CalledProcessError:
875 raise Exception() # XXX: needs error message
877 if completed.returncode == 0:
878 return completed.stdout.decode("utf-8").split("\n")[0]
879 else:
880 message = completed.stderr.decode("utf-8")
881 self.WriteFatal(f"Message from '{command} {' '.join(arguments)}': {message}")
882 raise Exception() # XXX: needs error message
884 # self.WriteFatal(f"Message from '{command}': {message}")
886 def GetProject(self, config: Project) -> Project:
887 return Project(
888 name=config.name,
889 version=config.version,
890 variant=config.variant
891 )
893 def GetBuild(self, config: Build) -> Build:
894 dt = datetime.now()
895 return Build(
896 date=dt.date(),
897 time=dt.time(),
898 compiler=self.GetCompiler(config.compiler)
899 )
901 def GetCompiler(self, config: Compiler) -> Compiler:
902 return Compiler(
903 name=config.name,
904 version=SemanticVersion.Parse(config.version),
905 configuration=config.configuration,
906 options=config.options
907 )
909 def GetEnvironment(self) -> Any:
910 env = {}
911 for key, value in environ.items():
912 if not key.isidentifier():
913 self.WriteWarning(f"Skipping environment variable '{key}', because it's not a valid Python identifier.")
914 continue
915 key = key.replace("(", "_")
916 key = key.replace(")", "_")
917 key = key.replace(" ", "_")
918 env[key] = value
920 def func(s) -> Generator[Tuple[str, Any], None, None]:
921 for e in env.keys():
922 yield e, s.__getattribute__(e)
924 Environment = make_dataclass(
925 "Environment",
926 [(name, str) for name in env.keys()],
927 # bases=(SelfDescriptive,),
928 namespace={
929 "as_dict": lambda self: env,
930 "Keys": lambda self: env.keys(),
931 "KeyValuePairs": lambda self: func(self)
932 },
933 # slots=True,
934 repr=True
935 )
937 return Environment(**env)
939 def FillOutTemplate(self, template: str, **kwargs) -> str:
940 # apply variables
941 try:
942 return template.format(**self._variables, **kwargs)
943 except AttributeError as ex:
944 self.WriteFatal(f"Syntax error in template. Accessing field '{ex.name}' of '{ex.obj.__class__.__name__}'.")